Jul 24 2008

equals() in Java

Die equals() Methode gehört in Java automatisch zur öffentlichen Schnittstelle einer jeden Klasse, da sie jede Klasse von Object automatisch erbt.

Da die Collections in Java viel Gebrauch von der equals() Methode machen, lohnt es sich mal einen tieferen Blick darauf zu werfen!

Theorie

Wann sollte equals() überschrieben werden?

equals() muss definitiv nicht für jede Klasse überschrieben werden! Vom Aufwand wäre es sogar am Einfachsten, sie gar nicht zu implementieren ;-) . Dann ist jede Instanz dieser Klasse nur zu sich selbst equal.

Grob gesagt kann man die equals() Methode bei Klassen weglassen, die “Dienste” anbieten und bei denen ihr Inhalt keine große Bedeutung hat. Ganz im Gegenteil bei Klassen, die bestimmte Werte repräsentieren. Diese Klassen sind auch Kandidaten für die Collections von Java und benötigen damit höchstwahrscheinlich eine eigens implementierte equals() Methode!

Der equals-Vertrag

Die folgenden Bedingungen kann man auch direkt der Dokumentation von Object entnehmen:

  • reflexiv: für x != null muss gelten, dass x.equals(x) true zurückgibt
  • symmetrisch: für x, y != null muss gelten, dass x.equals(y) nur true gibt, wenn auch y.equals(x) true gibt
  • transitiv: für x, y, z != null muss gelten, dass wenn x.equals(y) und y.equals(z) true ergibt, dann muss auch x.equals(z) true ergeben
  • konsistent: für x, y != null muss gelten, dass mehrmalige Aufrufe von x.equals(y) konsistent true oder false zurückgibt, sofern sich an den Objekten nichts ändert
  • x.equals(null) muss false zurückgeben (und keine NullPointerException!)

Sie beschreiben den Vertrag, den man als Programmierer dringenst einhalten sollte. Tut man das nicht, ist nicht zu 100% garantiert, dass sich z.B. die Collections Klasse so verhalten, wie sie sollen.

Die letzte Bedingung (x.equals(null) == false) lässt sich mit einem einfachen o instanceof Type prüfen, da instanceof schon direkt false liefert, wenn das Objekt o null ist!

Während Objekthierarchien durchaus mit super.equals(Object other) arbeiten können, dürfen direkte Subklassen von Object dies nicht tun! Da Object.equals(Object other) nur mittels == prüft, wird so gut wie immer false zurückgegeben, außer beide Objekte verweisen auf die gleiche Referenz. Das ist aber gerade dann, wenn man equals() überschreibt höchstwahrscheinlich nicht gewünscht.

Wird eine Unterklasse abgeleitet und dieser Klasse eine Membervariable hinzugefügt, dann lässt sich die Transitivität nicht mehr ohne weiteres herstellen! Die Symmetrie funktioniert noch, da die Member der Unterklasse beim Vergleich mit der Oberklasse ignoriert werden können. Aber bei der Transitivität müsste die Oberklasse wieder mit der Unterklasse verglichen werden und dann kommt man nicht mehr an die Member.

instanceof oder getClass()?

Es gibt im Prinzip 2 Lager: die instanceof-Verfechter und die, die getClass() favorisieren.

Mit getClass() stellt man sicher, dass definitiv beide Objekte (this und other) zur Laufzeit vom gleichen Typ sind (instanceof liefert ja auch beim Vergleich auf Oberklassen true). Man kann sich also ganz sicher sein, dass der equals-Vertrag eingehalten wird.

Allerdings wird argumentiert, dass die getClass() Methode das Substitutionsprinzip verletzt. Z.B. ist es nicht mehr möglich, von einer Klasse abzuleiten und nur Methoden zu überschreiben. Diese neue Unterklasse wird niemals equal mit ihrer Oberklasse sein.

Mit instanceof umgeht man dieses Problem und kann wieder das Substitutionsprinzip anwenden. Allerdings muss man obige Schilderung bedenken, dass man zu den abgeleiteten Klassen keine Member hinzufügen darf!

Allgemein bieten sich folgende Lösungen an:

  • Abstrakte Oberklassen, die keine eigenen Member definieren. Da man in diesem Fall die Oberklasse nicht direkt erzeugen kann.
  • Finale Klassen, die man erst gar nicht ableiten kann eignen sich natürlich hervorragend und arbeiten super mit instanceof zusammen. Diese Lösung ist sogar wahrscheinlich für fast alle “Werte”-Klassen optimal oder wer nutzt komplex verschachtelte Hierarchien von “Werte”-Klassen? Findet man sich darin noch zurecht? :-)

Implementierungsinfos

Ausgehend von der Theorie kann man mit folgenden “Punkten” die equals() Methode implementieren:

  1. == nutzen, um anfangs die eigene Identität zu prüfen. Das ist hauptsächlich eine Performance-Optimierung, die sich bei großen Objekten besonders auswirkt.
  2. Mit instanceof prüfen, ob das übergebene Objekt vom richtigen Typ ist (und damit wird ja auch gleich geprüft, ob das übergebene Objekt vielleicht null ist…).
  3. Das übergebene Objekt auf den eigenen Typ casten.
  4. Für jede signifikante Membervariable prüfen, ob sie bei beiden Objekten übereinstimmt.

    • Primitive Member werden mit == verglichen (außer float und double).
    • float mit Float.equals() und double mit Double.equals() vergleichen, da es z.B. Float.NaN gibt!
    • Referenztypen mit equals() vergleichen.
    • Wenn die Referenztypen null sein können, dann bietet sich folgendes Konstrukt an:

      • (field == null ?
            other.field == null :
                field.equals(other.field))
        
    • Und wenn dabei auch noch field und other.field identisch sein können, bietet sich das Konstrukt an:

      • (field == other.field ||
            (field != null && field.equals(other.field)))
        
  5. Am Ende sollte nochmals geprüft werden, ob die implementierte equals() transitiv, symmetrisch und konsistent ist.

Um die Performance noch etwas zu optimieren, könnem speziell unveränderbare (immutable) Objekte ihren kanonische Form vergleichen satt über alle Member zu gehen. Außerdem sollten die Member, die sich zwischen 2 Objekte oft unterscheiden anfangs überprüft werden, damit sich equals() in dem Fall schnell beenden kann.

Ganz wichtig: wann immer equals() überschrieben wird, muss auch hashCode() überschrieben werden!

Implementierungsbeispiel

public final class Person {
  private final Address address;
  private String prename;
  private int age;
  // ... more members ...

  // ... methods ...

  @Override
  public boolean equals(Object other) {
    if(other == this) {
      return true;
    }
    if(!(other instanceof Person)) {
      return false;
    }

    Person p = (Person) other;
    return this.age == p.age &&
      this.prename.equals(p.prename) &&
      (this.address == null ?
          p.address == null :
              this.address.equals(p.address)) &&
      // ... other comparisons ...
  }

  // ... methods ...
}

Literatur

Bücher

  • BLOCH, J.: Effective Java. Addison-Wesley, 2nd Edition, 2008.

Links