Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | NEWSLETTER | CONTACT | Twitter | Lanyrd | Linkedin
 
HOME 

  OVERVIEW

  BY TOPIC
    JAVA
    C++

  BY COLUMN
    EFFECTIVE JAVA
    EFFECTIVE STDLIB

  BY MAGAZINE
    JAVA MAGAZIN
    JAVA SPEKTRUM
    JAVA WORLD
    JAVA SOLUTIONS
    JAVA PRO
    C++ REPORT
    CUJ
    OTHER
 

GENERICS 
LAMBDAS 
IOSTREAMS 
ABOUT 
NEWSLETTER 
CONTACT 
Implementing the equals() Method - Part 2

Implementing the equals() Method - Part 2
Objektvergleich
Wie, wann und warum implementiert man die equals()-Methode?

Teil 2: Der Vergleichbarkeitstest

JavaSPEKTRUM, März 2002
Klaus Kreft & Angelika Langer

Dies ist das Manuskript eines Artikels, der im Rahmen einer Kolumne mit dem Titel "Effective Java" im JavaSPEKTRUM erschienen ist.  Die übrigen Artikel dieser Serie sind ebenfalls verfügbar ( click here ).

 

In diesem Artikel knüpfen wir an den vorangegangenen Artikel (siehe /KRE1/ ) über equals()-Implementierungen an. Nach einen kurzen Wiederholung der Hauptaspekte des letzten Artikels konzentrieren wir uns auf den Vergleichbarkeitstest, den man im Rahmen einer equals()-Implementierung durchführen muss.  Anhand von Beispielen aus Veröffentlichungen und dem JDK diskutieren wir die verschiedenen Aspekte des Vergleichbarkeitstest mittels getClass()-Methode und instanceof-Operator.

Rückblick

Objekte in Java kann man miteinander vergleichen, indem man die Methode equals() des einen Objekts aufruft und das andere Objekt als Argument mitgibt. Das Ergebnis ist ein boolscher Wert, der angibt, ob die beiden Objekte "gleich" sind. Ausnahmslos alle Klassen in Java definieren diese Methode.  Die Implementierung der equals()-Methode ist entweder von der Superklasse Object geerbt oder wurde in der betreffenden Klasse überschrieben.

Genau damit haben wir uns in der letzten Ausgabe dieser Kolumne beschäftigt:  wie implementiert man equals() in korrekter Art und Weise, wenn die Defaultimplementierung aus Object nicht das Richtige tut. Wenn man eine neue Klasse in Java entwirft, dann muss man neben all den anderen Design-Entscheidungen auch noch entscheiden, ob die Klasse equals() überschreiben muss oder nicht.  Wir haben zwischen Value- und Entity-Typen unterschieden und festgestellt, dass Value-Typen üblicherweise die equals()-Methode überschreiben müssen, wohingegen für Entity-Typen die geerbte Default-Implementierung aus Object ausreichend ist. Das liegt daran, dass Value-Objekte durch ihren Inhalt charakterisiert sind und "Gleichheit" im Sinne von equals() bedeutet daher für einen Value-Typ "Gleichheit des Inhalts".  Das ist bei Entity-Typen anders.  Dort bedeutet "Gleichheit", dass zwei Objekte identisch sind, d.h. nicht nur den gleichen Inhalt haben, sondern ein und dasselbe Objekt sind.

In der Implementierung der equals()-Methode ist man nicht völlig frei, sondern die Implementierung muss den sogenannten "equals()-Contract" erfüllen. Den equals()-Contract findet man in der JDK JavaDoc unter Object.equals(). Er enthält neben der Forderung der eigentlichen Funktionalität, nämlich dass equals() auf Gleichheit zweier Objekte prüfen soll, folgende 5 Regeln:

  • Jedes Objekt liefert beim Vergleich mit sich selbst true.
  • Es ist egal, ob man x mit y vergleicht, oder y mit x; das Ergebnis ist dasselbe.
  • Wenn x gleich y ist und y gleich z, dann sind auch x und z gleich.
  • Man kann zwei Objekte beliebig oft miteinander vergleichen; es kommt immer dasselbe heraus, solange sich die Objekte nicht verändern.
  • Alle Objekte sind von null verschieden.
Man sollte stets darauf achten, dass equals() konform zu diesen Regeln implementiert wird.  Wenn eine Implementierung davon abweicht, dann sind Probleme unvermeidbar, weil sich alle Benutzer von equals() intuitiv auf die Eigenschaften verlassen, die der equals()-Contract formal beschreibt.

Wir haben im letzten Artikel eine Anleitung zum Implementieren von  equals() gegeben.  Hier noch einmal die wesentlichen Elemente:
 

  • Signatur . Die equals()-Methode der eigenen Klasse sollte dieselbe Signatur haben wie Object.equals(), nämlich  public boolean equals(Object other).
  • Alias-Prüfung. Aus Optimierungsgründen kann man als erstes auf Identität von this und other prüfen.
public boolean equals(Object other) {
  if (this == other)
     return true;
  ...
}
  • Aufgabenteilung in Klassenhierarchien . Direkte und indirekte Subklassen von Object übernehmen unterschiedliche Aufgaben:
  • Indirekte Subklassen: Delegation an super . Die Prüfung, ob die geerbten Anteile des Objekts gleich sind, wird an die Superklasse delegiert.

  •  

     
     
     
     
     
     
     
     
     
     
     

    boolean equals(Object other) {
      ...
      if (!super.equals(other))
        return false;
      ...
    }

  • Direkte Subklassen: Test auf null . Es wird geprüft, ob other eine null-Referenz ist, damit sichergestellt ist, dass es nicht zu einer NullPointerException kommt.
public boolean equals(Object other) {
  ...
  if (other == null)
     return false;
  ...
}
  • Direkte Subklassen: Test auf Vergleichbarkeit . Es wird geprüft, ob this und other vergleichbar sind.  Das ist der Fall, wenn sie vom selben Typ sind. Wir haben folgenden Test vorgeschlagen:
    public boolean equals(Object other) {
      ...
      if (other.getClass() != getClass())
        return false;
      ...
    }

    Daneben gibt es andere Techniken, bei denen versucht wird, den Vergleich zwischen Sub- und Superobjekten zu erlauben.  Diese anderen Techniken und ihre Vor- und Nachteile wollen wir in diesem Artikel im Detail betrachten.

  • Vergleich der Felder . Nach den für direkte und indirekte Klassen unterschiedlichen Aufgaben folgt bei allen Klassen der eigentliche Vergleich der Felder.
Am Ende, wenn alle Test erfolgreich waren, wird true zurückgegeben.
 

Wenn man diesem Muster folgt, dann kann man sich beruhigt zurücklehnen; die resultierenden Implementierungen von equals() sind konform zum equals()-Contract und damit garantiert korrekt.  Allerdings gibt es Alternativen zu einigen der oben aufgeführten Aufgaben. Besonders heikel ist der Vergleichbarkeitstests, den wir mit Hilfe der getClass()-Methode vorgeschlagen haben. Im Folgenden sehen wir uns diese Alternativen näher an, insbesondere weil alternative Lösungen sehr häufig vorkommen und leider fast ebenso häufig inkorrekt oder stark situationsabhängig und deshalb fragil sind.
 

Test auf Vergleichbarkeit

Warum haben wir den Test auf Vergleichbarkeit überhaupt gemacht? Die equals()-Methode akzeptiert ein Argument vom Typ Object, d.h. eine Referenz auf ein Objekt eines beliebigen Typs (ausgenommen sind natürlich primitiven Typen, da sie nicht von Objekt abgeleitet sind). Mit anderen Worten, es ist zunächst einmal nicht sicher gestellt, dass this und other vom selben Typ sind und überhaupt miteinander verglichen werden können.  Deshalb macht man den Test auf Vergleichbarkeit.

Wir haben einen sehr strengen Test gemacht, nämlich den Test auf Typgleichheit per getClass():

public boolean equals(Object other) {
  ...
  if (other.getClass() != getClass())
    return false;
  ...
}

Mit dieser Implementierung von equals() sind nur Objekte vom gleichen Typ miteinander vergleichbar.  Das ist insofern etwas rigide, weil es unter Umständen auch Sinn machen kann, Objekte unterschiedlichen Typs miteinander zu vergleichen.  Beispielweise könnte man zulassen, dass Objekte von Sub- und Superklassen miteinander verglichen werden können.  Immerhin haben Sub- und Superobjekte etwas gemeinsam, das man miteinander vergleichen könnte, nämlich die Felder der Superklasse.
 

Vergleich von Sub- und Superobjekten

Betrachten wir also eine Klassenhierarchie und nehmen wir an, der Vergleichbarkeitstest in equals() sei so implementiert, wie wir es vorgeschlagen hatten, nämlich per getClass():

class Super {
 public boolean equals(Object other) {
  ...
  if (other.getClass() != getClass())  // Vergleichbarkeitstest
    return false;
  ...
 }
}

class Sub extends Super {
 public boolean equals(Object other) {
  ...
    if (!super.equals(other))         // Delegation an Superklasse
    return false;
  ...
 }
}

Man beachte, dass der Vergleichbarkeitstest nur in der direkten Subklasse von Object gemacht wird, weil nach der vorgeschlagenen Anleitung dieser Test wegen des rekursiven Aufrufs von equals() nur einmal, nämlich in der obersten Superklasse, gemacht werden muss. Dann liefert der Vergleich von Sub- und Superklassenobjekte immer das gleiche Ergebnis, nämlich false:

Super s1 = new Super();
Sub   s2 = new Sub();
... super.equals(sub) ...  // immer false

Das liegt daran, dass beim Vergleich geprüft wird, ob sub.getClass() gleich super.getClass() ist, und das immer falsch, weil die beiden Objekte tatsächlich von verschiedenem Typ sind. (getClass() ist eine Methode, die in Object definiert ist.  Sie liefert ein eindeutiges Objekt vom Typ Class zurück, welches den dynamischen Typ des Objekts (d.h. den Typ zur Laufzeit im Gegensatz zum statischen Typ zur Compilezeit) repräsentiert. Objekte von verschiedenem Typ haben verschiedene Class-Objekte.)

Um zu erreichen, dass der Vergleich true liefert, wenn der Superklassenanteil der beiden Objekte gleich ist, muss equals() anders implementiert werden.  Man prüft dann nicht mit der getClass()-Methode, sondern mit dem instanceof-Operator. (Der instanceof-Operator prüft, ob das Objekt  auf der linken Seite vom gleichen oder von einem Subtyp des Typs auf der rechten Seite ist.) Hier ist ein typisches Beispiel:

class Super {
 public boolean equals(Object other) {
  ...
  if (!other instanceof Super)
    return false;
  ...
 }
}
class Sub {
 public boolean equals(Object other) {
  ...
  if (!other instanceof Sub)
    return false;
  ...
 }
}

Damit besteht die Chance, dass der Vergleich auch einmal true liefert, nämlich dann, wenn die gemeinsamen Felder gleich sind.

Super s1 = new Super();
Sub   s2 = new Sub();
... super.equals(sub) ...  // möglicherweise true

Der Vergleichbarkeitstest in equals() prüft auf sub instanceof Super, d.h. ob das Objekt sub von einem Typ ist, der gleich oder abgeleitet ist vom Typ Super.  Das ist hier der Fall, und dann werden die übrigen Prüfungen durchgeführt, um schließlich zu entscheiden, ob sub und super "gleich" sind.

Man beachte, dass es bei der Verwendung des instanceof-Operators nicht genügt, den Test nur einmal in der obersten Superklasse zu machen, weil hier "hard-coded" der Name der Klasse, für die das equals() jeweils implementiert wird, in die Prüfung eingeht.
 

Das Symmetrie-Problem der instanceof-Lösung


Leider ist diese Implementierung inkorrekt: sie verletzt die Symmetrie-Anforderung aus dem equals()-Contract.  Wenn man nicht super mit sub vergleicht, sondern umgekehrt, dann kommt u.U. ein anderes Ergebnis heraus, was nicht sein darf.

Super s1 = new Super();
Sub   s2 = new Sub();
... super.equals(sub) ...  // möglicherweise true
... sub.equals(super) ...  // immer false

Der Vergleichbarkeitstest im ersten Aufruf von equals() prüft auf sub instanceof Super, d.h. ob das Objekt sub von einem Typ ist, der gleich oder abgeleitet ist vom Typ Super.  Das ist der Fall. Der Vergleichbarkeitstest im zweiten Aufruf von equals() prüft auf super instanceof Sub, d.h. ob das Objekt super von einem Typ ist, der gleich oder abgeleitet ist vom Typ Sub.  Das ist natürlich falsch; deshalb kommt hier immer false heraus, ganz egal, ob die beiden Objekte einen gleichen Superklassenanteil haben, oder nicht.  Das ist eine klare Verletzung der Symmetrieanforderung des equals()-Contract und diese Implementierung ist inkorrekt.

Wenn diese Lösung falsch ist, warum zeigen wir sie dann? Diese Art der Implementierung ist leider so weit verbreitet, dass wir sie in zahllosen Veröffentlichungen gesehen haben und auch in realem Source-Code, beispielsweise in frühen Versionen der JDK-Klasses.  Heute ist dieses Symmetrie-Problem allgemein bekannt, aber leider gibt es immer noch unzählige Beispiele inkorrekter und problematischer  Implementierungen von equals(), die zwar nicht alle asymmetrisch sind, sondern andere Probleme haben, aber man findet sie sowohl in der gängigen Literatur als auch in den Standard Bibliotheken des JDK.  Schauen wir uns also einfach mal um.

Stöbern in Büchern und Source-Code

Wir haben verschiedene Bücher aus dem Regal genommen und geöffnet.  Wir haben public-domain Java-Source-Code angesehen. Dabei haben wir auf Anhieb ein ganze Reihe von problematischen Implementierungen gefunden.  Wir haben nicht etwa verzweifelt nach pathologische Fällen oder ganz besonders miserablen Veröffentlichen gesucht, sondern wir haben wahllos zugegriffen.  Im Folgenden wollen wir eine Auswahl der gefundenen  "Stilblüten" zeigen und daran die Problem von equals()-Implementierungen diskutieren. Das tun wir nicht, weil die genannten Bücher so besonders schlecht sind  (das ist nicht der Fall; manche der Bücher sind sogar sehr, sehr gut und trotz der Fehler absolut empfehlenswert) oder weil wir natürlich immer alles besser wissen als James Gosling, sondern um zu zeigen, dass selbst Java-Gurus die Problematik korrekter equals()-Implementierungen und ihrer Folgen unterschätzen.  Die Materie ist nicht-trivial und man muss ganz sorgfältig nachdenken, wenn man sich zukünftigen Ärger nach Möglichkeit ersparen will.

Wir betrachten Beispiele aus "Program Development in Java" von Barbara Liskov und John Guttag (siehe /LIS/ ),  "Effective Java" von Joshua Bloch (siehe /BLO/ ), "Practical Java" von Peter Haggar (siehe /HAG/ ), und aus dem JDK 1.3 Sourcecode (siehe /JDK/ ; Autoren: James Gosling,  Arthur van Hoff, Alan Liu).   Listing 1 bis 4 zeigen die Beispiele.
 
 
Listing 1: Barbara Liskov, "Program Development in Java", Seite 182, siehe  /LIS/
public class Point3 extends Point2 {
 private int z; 
 ...
 public boolean equals(Object p) { // overriding definition
   if (p instanceof Point3) return equals((Point3)p);
   return super.equals();
 }
 public boolean equals(Point2 p) { // overriding definition
   if (p instanceof Point3) return equals((Point3)p);
   return super.equals();
 }
 public boolean equals(Point3 p) { // extra definition
   if (p==null || z!=p.z) return false;
   return super.equals();
 }
 ...
}
Listing 2: JDK 1.3, package java.util, class Date, Autoren: James Gosling, Arthur van Hoff, Alan Liu, siehe /JDK/
public class Date implements java.io.Serializable, Cloneable, Comparable {
   private transient Calendar cal;
   private transient long fastTime;
   private static Calendar staticCal = null;
   // ... lots of static fields ...
   ...
   public long getTime() {
       return getTimeImpl();
   }
   private final long getTimeImpl() {
       return (cal == null) ? fastTime : cal.getTimeInMillis();
   }
   ...
   public boolean equals(Object obj) {
       return obj instanceof Date && getTime() == ((Date) obj).getTime();
   }
}
Listing 3: Josh Bloch, "Effective Java", Item 7 und Item 8, siehe /BLO/
public final class PhoneNumber {
 private final short areaCode;
 private final short exchange;
 private final short extension;
 ...
 public boolean equals(Object o) {
   if (o==this)
      return true;
   if (!(o instanceof PhoneNumber))
      return false;
   PhoneNumber pn = (PhoneNumber)o;
   return pn.extensions == extension &&
          pn.exchange   == exchange  &&
          pn.areaCode   == areaCode;
 }
 ...
}
Listing 4: Peter Haggar, "Practical Java", Praxis 8 bis Praxis 14, siehe /HAG/
class Golfball {
  private String brand;
  private String make;
  private int compression;
  ...
  public String brand() {
    return brand;
  }
  ...
  public boolean equals(object obj) {
    if (this == obj)
       return true;
    if (obj!=null && getClass() == obj.getClass())
    {
       Golfball gb = (Golfball)obj; // Classes are equal, downcast.
       if (brand.equals(gb.brand())  &&  // Compare atrributes.
           make.equals(gb.make()) &&
           compression == gb.compression())
         return true;
    }
    return false;
  }
}
class MyGolfball extends Golfball {
 public final static byte TwoPiece = 0;
 public final static byte ThreePiece = 1;
 private byte ballConstruction;
 ...
 public byte constuction() {
   return ballConstruction;
 }
 ...
 public boolean equals(Object obj) {
   if (super.equals(obj))
   {
     MyGolfball bg = (MyGolfball)obj;  // Classes equal, downcast.
     if (ballConstruction == gb.construction())
       return true;
   }
   return false;
 }
}

Sehen wir uns die Beispiele einmal der Reihe nach an.
 

Listing 1: Eine inkorrekte Version von equals()

Schon auf den ersten Blick fällt auf, dass Barbara Liskov eine interessante Kombination von Überladen und Überschreiben verwendet.  Da gibt es nicht nur eine Version von equals(), sondern gleich mehrere, mit unterschiedlichen Signaturen.  So kann man's auch machen.  Das hier vorgeschlagen Verfahren ist aber ein relativ ungewöhnlicher Ansatz, der sowohl Vor- als auch Nachteile hat.  Das wollen wir hier aber gar nicht diskutieren. Unser Interesse gilt im Moment dem Vergleichbarkeitstest.

Wenn man genau hinsieht, erkennt man, dass in dieser Implementierung equals() nicht transitiv  und damit inkorrekt ist.  Das Beispiel in Listing 1 zeigt eine Subklasse Point3, die von einer Superklasse Point2 abgeleitet ist. Die Klasse Point3 hat drei überladene Versionen von equals() zu bieten. Schauen wir mal, was passiert, wenn man diese equals()-Methoden benutzt. Nehmen wir an, wir haben Point2- und Point3-Objekte mit gleichem Superklassenanteil:
 
Point2 origin(0,0);
Point3 p1(0,0,-1);
Point3 p2(0,0,1);

Wenn wir diese Objekte miteinander vergleichen, ergibt sich das Folgende:

System.out.println(p1.equals(origin));  // ruft Point3.equals(Point2)
System.out.println(origin.equals(p2));  // ruft Point2.equals(Point2)
System.out.println(p1.equals(p2));      // ruft Point3.equals(Point3)

Man würde erwarten, dass alle drei Objekte gleich sind, weil sie den gleichen Superklassenanteil enthalten, und daher folgende Ausgabe erwarten:

true
true
true
Statt dessen ergibt sich folgende Ausgabe:
true
true
false
Gemäß dieser equals()-Implementierung sind die Point3-Objekte p1 und p2 nicht gleich, obwohl sie beide gleich mit dem Point2-Objekt origin sind. Nach den Regeln der Transitivität müssten die Point3-Objekte p1 und p2 aber gleich sein, da der Vergleich mit einem dritten Objekt in beiden Fällen true liefert.  Diese equals()-Implementierung ist nicht transitiv und verletzt den equals()-Contract und ist damit inkorrekt.  Ist das ein Problem?  Es ist doch offensichtlich, dass p1 und p2 nicht gleich sind.  Vielleicht ist der ganze equals()-Contract Blödsinn ... ?!?!?

Der equals()-Contract ist keinesfalls blödsinnig; das Beispiel ist vielleicht etwas zu simpel.  Man stelle sich eine geringfügig komplexere Situation vor, in der drei Superklassen-Referenzen miteinander verglichen werden, ohne dass man weiß, welche Art von Objekt da genau referenziert wird.  Wenn man drei Point2-Referenzen miteinander vergleicht und zwei der Vergleiche true liefern, aber der dritte liefert plötzlich false, dann ist das schon sehr überraschend und kann leicht zu Fehlern führen.

Worin besteht also genau das Problem?  Die Verletzung der Transitivitätsanforderung stammt daher, dass hier verschiedene Implementierungen von equals() gerufen werden und diese verschiedenen Implementierungen unterschiedliche Semantik haben.  Sehen wir uns das noch einmal im Detail an.

Im ersten Vergleich p1.equals(origin) wird die Methode Point3.equals(Point2) gerufen.  Sie ist wie folgt implementiert:

public class Point3 extends Point2 {
  ...
  public boolean equals(Point2 p) { // overriding definition
    if (p instanceof Point3) return equals((Point3)p);
    return super.equals();
  }
  ...
}

Die Methode Point3.equals(Point2) prüft, ob das andere Objekt (also origin) von einem Typ ist der ein Subtyp von Point3 ist.  Das ist falsch; es ist genau anders herum: der Typ von origin ist der Supertyp von this. Das Ergebnis des Test ist also false; dann wird super.equals() gerufen.  Die Implementierung von super.equals(), welches  in diesem Fall Point2.equals(Point2) ist, wird in Listing 1 nicht gezeigt, aber man kann annehmen, dass sie der gleichen Logik folgt.  Die Methode Point2.equals(Point2) vergleicht Point2-Objekte miteinander, in diesem Falle origin mit dem Superklassenanteil von p1. Das Ergebnis ist erwartungsgemäß true. Dasselbe ergibt sich, wenn origin mit p2 verglichen wird. Beide Vergleiche sind gemischte Vergleiche zwischen Super- und Subobjekten.  Die Semantik dieser "mixed-type"-Vergleiche ist der Vergleich der Superklassenanteile der beteiligten Objekte, was wir im Folgenden als "slice comparison" bezeichnen werden.

Für den  dritten Vergleich p1.equals(p2) wird die Methode Point3.equals(Point3) gerufen.  Dieser Vergleich müsste nun nach den Regeln der Transitivität true liefert.  Das tut er aber nicht.  Die beiden Objekte sind ja auch nicht gleich.  Was stimmt hier also nicht?  Die Methode Point3.equals(Point3) führt einen ganz anderen Vergleich durch als die Methode Point3.equals(Point2), die in den beiden "mixed-type"-Vergleichen verwendet wurde. Die Methode Point3.equals(Point3) macht einen "same-type"-Vergleich, d.h. sie vergleicht Objekte desselben Typs und vergleicht dabei die gesamten Objekte, nicht nur den Superklassenanteil davon.  Semantisch ist das ein ganz anderer Vergleich.

Und genau hier liegt das Problem.  Alle beteiligten Methoden heißen equals(), aber sie machen ganz unterschiedliche Dinge.  Je nach Konstellation wird mal die Superklassen-Variante von equals() gerufen, die beim "mixed-type"-Vergleich nur Superklassenanteile vergleicht, und mal wird die Subklassen-Variante von equals() gerufen, die alle Felder in Betracht zieht und deshalb zu anderen, inkompatiblen Ergebnissen kommt.  Es ist konzeptionell nicht möglich, einen korrekten, transitiven Vergleich  von Sub- und Superklassenobjekte zu definieren, wenn mal nur die Superklassenanteile und mal die gesamten Subobjekte betrachtet werden.  Der Widerspruch ist weder theoretisch noch praktisch aufzulösen.  Das heißt, solche Implementierungen wie die aus Listing 1 sind immer inkorrekt, da sie nicht transitiv sind.

Wenn man "mixed-type"-Vergleiche in Klassenhierarchien zulassen will, dann müssen alle Vergleiche in der gesamte Hierarchie dieselbe Semantik haben.  Man könnte beispielsweise eine isEqualToPoint2()-Methode definieren, die in der Superklasse Point2 als final definiert ist und daher in Subklassen nicht überschrieben werden kann. Diese isEqualToPoint2()-Methode würde die Point2-Anteile von Point-Objekten jeder Art vergleicht.  Das wäre auf allen Stufen der Hierarchie derselbe Vergleich.  Dann wären auch p1 und p2 aus unserem Beispiel "gleich" im Sinne von isEqualToPoint2(), weil ja nur die Superklassenanteile verglichen würden.  Eine solche Vergleichsmethode erfüllt alle Kriterien des equals()-Contract, mit der winzigen Einschränkung, dass sie einen vielleicht etwas eigenartigen Begriff von Gleichheit implementiert, nämlich die Gleichheit des Superklassenanteils aller Point-Objekte. Deshalb haben wir die Methode auch nicht equals() genannt.

Fazit: Symmetrische Implementierungen von equals(), die den Vergleich von Sub- und Superobjekten per instanceof-Operator zulassen und in den verschiedenen Stufen der Klassenhierarchie überschrieben werden, sind immer intransitiv und damit inkorrekt. Implementierungen von equals(), die nur den Vergleich von Objekten gleichen Typs per getClass()-Methode zulassen, sind dagegen unkritisch: sie haben das oben beschilderte  Intransitivitätsproblem nicht.
Asymmetrische Implementierungen von equals() mit instanceof haben wir bis jetzt noch nicht betrachtet; das tun wir im nächsten Abschnitt.  Asymmetrische Implementierungen können transitiv sein, aber sie sind natürlich auch nicht korrekt, wegen der fehlenden Symmetrie.

Listing 2: Eine weit verbreitete, aber dennoch fragwürdige Version von equals()

Nun, die Implementierung in Listing 1 war ohnehin etwas exotisch, schon allein durch den Mix von Überladen und Überschreiben.  Vielleicht war das ja ein Ausreißer.  Schauen wir uns mal was Reelles an und sehen uns Klassen aus den JDK-Bibliotheken an.  Ein typisches Beispiel ist in Listing 2 gezeigt.

Es handelt sich um das Beispiel einer non-final Klasse die einen Value-Typ repräsentiert, nämlich die Klasse Date. Die Klasse Date mit ihrer Implementierung der equals()-Methode ist zunächst einmal korrekt, so wie sie ist.  Probleme sind aber zu erwarten, sobald jemand von der Klasse Date ableitet.  Man beachte, dass die Klasse Date offenbar bewusst als potentielle Superklasse deklariert ist; sie ist nämlich nicht als final deklariert.  Dann leiten wir doch einmal eine Subklasse ab und sehen, was passiert.

Nehmen wir an, die Subklasse hat zusätzliche Felder und muss aus diesem Grunde die Implementierung von Date.equals() überschreiben, so dass auch die neuen Felder in den Vergleich eingehen.  Hier ist eine vorstellbare Subklasse NamedDate:

public class NamedDate extends Date {
  private String name;
  public boolean equals(Object other) {
    if (other instanceof NamedDate && !name.equals(((NamedDate)other).name))
       return false;
    return super.equals(other));
  }
}

Natürlich kann man NamedDate.equals() auch anders implementieren, aber wir folgen hier dem Stil, den die Superklasse Date nahelegt. Zur Erinnerung, hier die Implementierung von Date.equals():

public class Date {
  ...
  public boolean equals(Object obj) {
    return obj instanceof Date && getTime() == ((Date) obj).getTime();
  }
}

Beide Versionen von equals() benutzen den instanceof-Operator und lassen den "mixed-type"-Vergleich zu.  Betrachten wir ein Beispiel:
 
NamedDate EndOfMillenium = new NamedDate(99,11,31,"end of 2nd millenium AC");
NamedDate TheEnd         = new NamedDate(99,11,31,"end of the world");
Date      NewYearsEve    = new Date(99,11,31);

EndOfMillenium.equals(NewYearsEve)   // slice comparison: true
NewYearsEve.equals(TheEnd)           // slice comparison: true
EndOfMillenium.equals(TheEnd)        // whole-object comparison: false

Wir beobachten dasselbe Transitivitätsproblem wie in Listing 1.  Das liegt daran, dass Date.equals() den Vergleich von Sub- mit Superobjekten erlaubt, aber jede Subklasse mit Wahrscheinlichkeit equals() überschreiben wird und damit einen semantisch anderen Vergleich implementieren wird.  Und dann gibt es die oben schon ausführlich geschilderten Probleme.

Vielleicht war es ja falsch, die equals()-Methode der Subklasse NamedDate mit Hilfe des instanceof-Operators zu implementieren.  Wie wäre es denn, wenn man NamedDate.equals() mit Hilfe von getClass() implementierte?  Das wäre auch inkorrekt, denn dann  wäre equals() nicht mehr symmetrisch. Man könnte Date-Objekte zwar mit NamedDate-Objekten per Date.equals() vergleichen, aber umgekehrt per NamedDate.equals() geht es nicht, oder genauer gesagt, es kommt immer false heraus, egal ob die beiden Objekte die gleichen Superklassenfelder haben oder nicht.  Das ist eine Verletzung der Symmetrie-Anforderung aus dem equals()-Contract und damit also auch falsch.

Die Autoren dieser Date-Klasse hätten ihren Nutzern den Ärger mit inkorrekten Implementierungen von equals() in Subklassen ersparen können und die Date-Klasse oder zumindest die Methode Date.equals() als final deklarieren können.  Dann wäre die Klasse Date keine Superklasse oder aber man könnte zwar ableiten, aber equals() nicht überschreiben. In beiden Fällen wäre das Transitivitätsproblem ausgeschlossen.  Leider habe die Autoren das nicht getan.  Also muss man als JDK-Benutzer aufpassen, dass man nicht in diese Falle tappt: man sollte von solchen Bibliotheksklassen wie Date, die in ihrer Implementierung von equals() den instanceof-Operator verwenden, nur mit allergrößter Vorsicht ableiten.  Wenn man ableiten will, muss  man auf jeden Fall erst einmal den Source-Code der anvisierten Superklasse studieren und feststellen, wie equals() dort überhaupt implementiert ist; in der JavaDoc steht dazu üblicherweise überhaupt nichts.

Wenn man dann weiß, dass das equals() der Klasse, von der man ableiten will, den instanceof-Operator verwendet, dann macht eine Subklasse nur Sinn, wenn sie keine zusätzlichen Felder definiert und auch sonst nichts hinzufügt, was eine neue Version von equals() erfordern würde.  Nur dann, wenn  das Superklassen-equals() in der Subklasse nicht überschrieben werden muss, macht eine Subklasse überhaupt Sinn. (Tipp: als Autor einer solchen Subklasse sollte man mit Rücksicht auf seine eigenen Nutzer den Fehler nicht wiederholen und seine Subklasse als final deklarieren oder ein Dummy-equals() implementieren, welches final ist und nichts weiter tut, als an super.equals() zu delegieren.)

Man fragt sich, warum diese Fallen im JDK so zahlreich sind.  Die Klasse Date ist lediglich ein wahllos herausgegriffenes Beispiel; es gibt viele davon im JDK 1.3.  Einer der Java-Gurus bei Sun (nämlich Joshua Bloch, der Autor von "Effective Java" und Entwickler des Collection-Frameworks) hat dazu in einem privaten Email gesagt, man habe ja erst vor kurzem überhaupt erkannt, dass das ein Problem sei mit dem instanceof-Test in den non-final Klassen. Und außerdem sei das ja eigentlich auch gar kein Problem; es käme schließlich so gut wie nie vor, dass man beim Ableiten neue Felder hinzufügt.  (Siehe dazu auch Item 14 aus seinem Buch /BLO/ .)  So selten ist das Hinzufügen von Feldern beim Definieren von Subklassen unserer Erfahrung nach nicht, aber für gewisse Projekte mag Joshua Bloch's Einschätzung trotzdem richtig sein.

Wir würden aber dennoch empfehlen, den Implementierungsstil von JDK-Klassen wie Date für eigene Klassen nicht bedenkenlos nachzuahmen.  Man vermeidet viel Aufwand und Kopfschmerzen, wenn man Klassen gar nicht erst als potentielle Superklassen zulässt, d.h. eine Klasse sollte normalerweise final sein, es sei denn, sie ist wirklich als Superklasse gemeint.  Das Design von Superklassen ist deutlich aufwendiger und schwieriger als das Design "normaler" non-final Klassen.  Man muss nicht nur bei der equals()-Methode von Superklassen besonders aufpassen; das gleiche gilt auch für die clone()-Methode eine Superklasse. Es muss generell fein säuberlich dokumentiert werden, wie und wann und unter welchen Umständen non-final Methoden in den Implementierungen anderer Methoden der Superklasse verwendet werden und was von diesen non-final Methoden erwartet wird; schließlich muss das der Autor einer Subklasse wissen und beachten, falls er die Methoden in seiner Subklasse überschreibt. Da in Java alle Klassen per Default non-final und damit potentielle Superklassen sind, passiert es relativ häufig, dass der Autor einer non-final Klasse überhaupt nicht darüber nachgedacht hat, ob seine Klasse als Superklasse taugt, sondern die Klasse ist nur "zufällig" eine potentielle Superklasse. Ob eine solche Klasse zum Ableiten geeignet ist, ist fraglich.

Zurück zu unseren Literaturstudien.  Sehen wir uns die verbleibenden beiden Beispiele an.

Listing 3: Eine korrekte Version von equals()

Das ist das Beispiel einer PhoneNumber-Klasse, die man im Buch von Joshua Bloch findet. Die Implementierung von equals() verwendet zwar den instanceof-Test, aber der ist hier unproblematisch, weil die Klasse final ist. Joshua Bloch vermeidet so alle oben diskutierten Transitivitätsprobleme.  Die Lösung ist korrekt, aber in ihrer Benutzbarkeit auf final-Klassen beschränkt.
 

Listing 4: Noch eine korrekte Version von equals()

Das ist ein Beispiel aus dem Buch von Peter Haggar.  Wir sehen in Listing 4 das Beispiel einer Superklasse Golfball und ihrer Subklasse MyGolfball. Die Subklasse überschreibt equals(), weil sie Subklassen-spezifische Felder hat.  Die Superklasse benutzt nicht den instanceof-Test, sondern verwendet getClass():

class Golfball {
   ...
   public boolean equals(object obj) {
     if (this == obj)
        return true;
     if (obj!=null && getClass() == obj.getClass())
     {
        Golfball gb = (Golfball)obj; // Classes are equal, downcast.
        if (brand.equals(gb.brand())  &&  // Compare atrributes.
            make.equals(gb.make()) &&
            compression == gb.compression())
          return true;
     }
     return false;
   }
}
class MyGolfball extends Golfball {
  ...
  public boolean equals(Object obj) {
    if (super.equals(obj))
    {
      MyGolfball gb = (MyGolfball)obj;  // Classes equal, downcast.
      if (ballConstruction == gb.construction())
        return true;
    }
    return false;
  }
}

Das ist genau die Art von Implementierung, die wir empfehlen würden. Der fundamentale Unterschied zu all den anderen betrachteten Beispielen ist, dass hier der Vergleich von Golfball- mit MyGolfball-Objekten grundsätzlich fehlschlägt.  Hier ist ein Beispiel:
 
Golfball original = new Golfball("xyz", "abc",100);
MyGolfball gb1 = new MyGolfball("xyz", "abc", 100, MyGolfball.TwoPiece);
MyGolfball gb2 = new MyGolfball("xyz", "abc", 100, MyGolfball.ThreePiece);

gb1.equals(original)  // mixed-type comparison: yields false
original.equals(gb2)  // mixed-type comparison: yields false
gb1.equals(gb2)       // same-type comparison:  yields false

Diese Art der Implementierung ist korrekt und erfüllt alle Anforderungen des equals()-Contract.  In Klassenhierarchien, in denen equals() überschrieben werden kann und muss, ist das die sinnvollste Strategie.

Anmerkung: Man kann im übrigen die beiden Implementierungstechniken (instanceof vs. getClass()) nicht innerhalb einer Klassenhierarchie mischen.  Würden wir beispielsweise versuchen, die NamedDate-Klasse (siehe Listing 2 und anschließende Diskussion)  mit getClass() zu implementieren, während die Superklasse Date den instanceof-Operator verwendet, dann produzieren wir ein asymmetrisches equals() für diese Klassenhierarchie: ein NamedDate ließe sich mit einem Date vergleichen, aber nicht umgekehrt.  Der Designer der Superklasse trägt also große Verantwortung: er legt die Implementierungsstrategie für die equals()-Methoden in der gesamte Hierarchie fest.
 

Schlussfolgerung

Es gibt zwei Möglichkeiten, den Vergleichbarkeitstest in equals() zu machen: mit Hilfe der getClass()-Methode oder mit Hilfe des instanceof-Operators.
  • Die getClass()-Technik ist robust und unproblematisch und daher zu empfehlen.
  • Die instanceof-Technik ist weit verbreitet, macht aber nur bei Klassen Sinn, die final sind.


Man findet in der Literatur und auch in der Praxis viele non-final Klassen, die trotz der Transitivitätsprobleme den instanceof-Test in ihrer non-final Methode equals() machen. Das Ableiten von solchen Klassen ist äußerst fehleranfällig und sollte grundsätzlich vermieden werden. (Man kann in vielen Fällen bei der Definition neuer Klassen Ableitung durch Delegation ersetzen; es muss nicht immer Vererbung sein.)

Für die Implementierung eigener Klassen hält man sich am besten an die obige Regel: bei final-Klassen ist es egal, wie man den Vergleichbarkeitstest in equals() macht, und bei non-final Klassen nehme man getClass().
 

Ausblick

Wir haben in diesem und dem vorangegangenen Artikel einige Aspekte der Implementierung von equals() besprochen.  Das Thema ist damit noch nicht erschöpfend behandelt.  Wir haben zum Beispiel bislang kaum erwähnt, dass equals() nicht allein auf der Welt ist und Querbezüge zu anderen Infrastruktur-Methoden wie hashCode() und compareTo() hat. Die Implementierungen dieser Methoden müssen konsistent zu equals() sein.  Mehr darüber beim nächsten Mal (/ KRE3 /).
 

Nachtrag

Viele Jahre später - dieser Zusatz wurde im Juli 2009 geschrieben, also 7 Jahre nach der Veröffentlichung des obigen Artikels  - wird das Thema equals() noch immer diskutiert.  Mittlerweile hat sich augenscheinlich herumgesprochen, dass die Implementierung von equals() vielleicht doch nicht ganz so simpel ist, wie sie in Joshua Bloch's  "Effective Java" auf den ersten Blick erscheinen mag.  Diesmal wird das Thema aus dem Blickwinkel von Scala aufgerollt, rückübersetzt nach Java.  Dem interessierten Leser sei daher folgender Beitrag als Ergänzung empfohlen: " How to Write an Equality Method in Java " von Martin Odersky, Lex Spoon und Bill Venners vom 1. Juni 2009 (/ OSV /) .
 

Literaturverweise

/KRE1/  Wie, wann und warum implementiert man die equals()-Methode? 
Teil 1: Die Prinzipien der Implementierung von equals()
Klaus Kreft & Angelika Langer
Java Spektrum, Januar 2002
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/01.Equals-Part1/01.Equals1.html
/KRE2/ Wie, wann und warum implementiert man die equals()-Methode? 
Teil 2: Der Vergleichbarkeitstest 
Klaus Kreft & Angelika Langer
Java Spektrum, März 2002
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/02.Equals-Part2/02.Equals2.html
/KRE3/ Wie, wann und warum implementiert man die hashCode()-Methode? 
Klaus Kreft & Angelika Langer
Java Spektrum, Mai 2002
URL: http://www.AngelikaLanger.com/Articles/EffectiveJava/03.HashCode/03.HashCode.html
/KRE4/ Secrets of equals()
Part 1: Not all implementations of equals() are equal
Angelika Langer & Klaus Kreft
Java Solutions, April 2002
URL: http://www.AngelikaLanger.com/Articles/Java/SecretsOfEquals/Equals.html
/KRE5/ Secrets of equals()
Part 2: How to implement a correct slice comparison in Java
Angelika Langer & Klaus Kreft
Java Solutions, August 2002
URL: http://www.AngelikaLanger.com/Articles/Java/SecretsOfEquals/Equals-2.html
/OSV/ How to Write an Equality Method in Java
Martin Odersky, Lex Spoon und Bill Venners
artima developer,  Juni 2009
URL: http://www.artima.com/lejava/articles/equality.html
/DAV/ Durable Java: Liberté, Égalité, Fraternité
Mark Davis 
Java Report, January 2000 
URL: http://www.macchiato.com/columns/Durable5.html
/BLO/  Effective Java Programming Language Guide
Josh Bloch 
Addison-Wesley, June 2001 
ISBN: 0201310058
/BLO2/ Joshua Bloch's comment on instanceof versus getClass in equals methods:
A Conversation with Josh Bloch
by Bill Venners
URL: http://www.artima.com/intv/bloch17.html
/HAG/  Practical Java: Programming Language Guide
Peter Haggar 
Addison-Wesley, March 2000 
ISBN 0201616467
/LIS/  Program Development in Java: Abstraction, Specification, and Object-Oriented Design
Barbara Liskov with John Guttag 
Addison-Wesley, January 2000 
ISBN: 0201657686
/GOF/  Design Patterns: Elements of Reusable Object-Oriented Software
Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides 
Addison-Wesley, January 1995 
ISBN: 0201633612
/JDK/  Java 2 Platform, Standard Edition v1.3.1
URL: http://java.sun.com/j2se/1.3/
/JDOC/  Java 2 Platform, Standard Edition, v 1.3.1 - API Specification
URL: http://java.sun.com/j2se/1.3/docs/api/index.html

 
 

If you are interested to hear more about this and related topics you might want to check out the following seminar:
Seminar
 
Effective Java - Advanced Java Programming Idioms 
4 day seminar ( open enrollment and on-site)
 
  © Copyright 1995-2009 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/02.Equals-Part2/02.Equals2.html  last update: 1 Jul 2009