Angelika Langer - Training & Consulting
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | 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 
CONTACT 
Implementing the equals() Method - Part 1

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

Teil 1: Die Prinzipien der Implementierung von equals()

JavaSPEKTRUM, Januar 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 ).

 
 

Vorbemerkung

Mit diesem Artikel beginnen wir unter dem Titel "Effective Java" eine Kolumne, die sich mit der Programmiersprache Java auseinandersetzen wird.  Wir haben dabei bewusst den Titel "Effective Java" gewählt, um an die Tradition von Scott Meyers anzuknüpfen, der den Begriff "Effective..." durch seine "Effective C++"-Bücher populär gemacht hat.  Betrachtungen unter dem Motto "Effective" wenden sich typischerweise der Tücke des Objekts zu und so werden wir uns in dieser Kolumne den mehr oder weniger offensichtlichen Fallstricken der Programmiersprache Java widmen.

Nun werden unter dem Begriff "Java" unzählige Aspekte subsummiert, von Realtime-Programmierung unter speziellen virtuellen Maschinen über GUI-Programmierung bis hin zur Applikationsentwicklung auf Basis von EJB und JSP.  Es wäre vermessen, über "Java" in dieser Gesamtheit schreiben zu wollen.  Wir beschränken uns daher bewusst auf den Kern von Java:  die Programmiersprache selbst, wesentliche Aspekte der virtuellen Maschine und einige grundlegende APIs aus den Bibliotheken der Java 2 Standard Edition (J2SE).  Unser Ziel ist es, über genau den Teil von Java zu schreiben, der jeden Java-Programmierer angeht, ganz egal in welcher Domain er oder sie programmiert.  Dabei wollen wir uns die weniger offensichtlichen und bisweilen überraschenden Effekte in Java ansehen.
 

Objekt-Infrastruktur in Java


Beginnen wir mit scheinbaren Trivialitäten wie "Kopieren von Objekten" und "Vergleichen von Objekten".  Gemeint sind die Methoden clone(), equals() und einige andere, die zusammen so etwas wie die "Infrastruktur" eines Objekts ausmachen.  Was meinen wir mit "Infrastruktur"?

Alle Klassen in Java sind implizit von der Klasse Object abgeleitet und erben daher alle Methoden aus Object.   Zu diesen geerbten Methoden gehören die public Methoden equals() und hashCode(). equals() vergleicht zwei Objekte miteinander, während hashCode() einen integralen Wert (den sogenannten Hashcode) berechnet.  Mit den Details dieser Methoden werden wir uns in diesem und den folgenden Artikeln  noch eingehend beschäftigen.  An dieser Stelle nur soviel: beide Methoden werden u.a. gebraucht, um Java-Objekte in hash-basierten Containern wie beispielsweise HashSet ablegen zu können.

Wegen der automatischen Ableitung von der Superklasse Object sind equals() und hashCode()  Teil der Schnittstelle einer jeder Java-Klasse, d.h. man kann auf allen Objekten in Java equals() und hashCode()aufrufen.  Es gibt auch immer eine Implementierung dieser Methoden, nämlich entweder die aus Object geerbte Default-Implementierung oder eine klassenspezifische Implementierung, wenn die betreffende Klasse die geerbte Methode überschrieben hat.

Methoden wie equals() und hashCode() stellen Basisfunktionalität zur Verfügung, die man von allen Objekten in Java erwartet.   Die Menge der Basisfunktionalitäten bezeichnet man bisweilen als "Infrastruktur" eines Objekts.  Zur Infrastruktur gehören nicht nur equals() und hashCode(), sondern auch Funktionalität für Initialisierung und Aufräumen von Objekten sowie für Kopieren und Vergleichen von Objekten. Initialisierung geschieht üblicherweise mittels Konstruktoren, Aufräumen mittels finalize()-Methode, Kopieren mittels clone()-Methode, Vergleichen mittels equals() und compareTo()-Methode.  Die Liste erhebt keinen Anspruch auf Vollständigkeit.  Zur Infrastruktur gehören in gewissem Sinne auch die Methoden für die Serialisierung von Objekten, nämlich readObject() und writeObject(), weil sie ebenfalls so etwas wie Konstruieren und Kopieren von Objekten definieren.  Die von einer Klasse geforderte Infrastruktur kann also variieren abhängig vom Kontext, in dem die Klasse verwendet werden soll.

Wie wir bereits gesehen haben, werden equals() und hashCode() von der Superklasse Object geerbt.  Beides sind public Methoden in Object, d.h. equals() und hashCode() gehören immer zur Schnittstelle einer Klasse.  Das ist anders bei clone() und finalize().  Diese beiden Methoden sind ebenfalls in der Superklasse Object definiert, aber sie sind dort als protected deklariert.  Damit werden sie zwar geerbt, sind aber nicht automatisch Bestandteil der Schnittstelle der Subklasse.  Nur wenn die Subklasse Funktionalität für das Kopieren oder Aufräumen unterstützen will, dann wird sie diese geerbten Methoden aus Object überschreiben und als eigene public Methoden zur Verfügung stellen.  (Im Falle von clone() kommt noch hinzu, dass die Subklasse zusätzlich das Cloneable-Interface implementieren muss, damit die clone()-Methode funktioniert.)

Andere Teile der Infrastruktur haben gar nichts mit der Superklasse Object zu tun, sondern man implementiert gewisse Interfaces, um die entsprechende Infrastruktur zur Verfügung zu stellen.  In diese Kategorie fallen die Methoden compareTo() aus dem Comparable-Interface und readObject() und writeObject(), aus dem Serializable-Interface.  Diese Teile der Infrastruktur wird eine Klasse nur dann zur Verfügung stellen, wenn das sinnvoll erscheint, was allerdings häufig der Fall ist:  wenn Objekte in baum-basierten Containern wie TreeSet abgelegt werden sollen, dann macht es sehr viel Sinn, dass die Klasse eine compareTo()-Methode bekommt.  Analog, wenn Objekte serialisiert werden sollen, dann müssen readObject() und writeObject() implementiert werden.

Damit haben wir nun eine Liste von Basisfunktionalität, die jede Java-Klasse zur Verfügung stellen kann. Beim Design einer neuen Klasse muss entschieden werden, welche Teile der Infrastruktur unterstützt werden sollen. Gewisse Methoden, nämlich equals() und hashCode(), können gar nicht vermieden werden.  Wenn eine Klasse diese Methoden nicht überschreibt, dann steht automatisch die Default-Funktionalität aus der Superklasse Object zur Verfügung. Für diese Methoden ist die entscheidende Frage nicht "Unterstützen? Ja oder Nein?", sondern man muss entscheiden: "Ist das Default-Verhalten korrekt? Ja oder Nein?".  Die Entscheidungen, die der Autor einer Klasse an dieser Stelle trifft, haben weitreichende Auswirkungen für die Benutzung und Benutzbarkeit der Klasse.  Das gilt ganz besonders, wenn die Klasse eine potentielle Superklasse ist, und jede Klasse in Java, die nicht als final erklärt ist, ist eine potentielle Superklasse.

In dieser und den nachfolgenden Ausgaben der Kolumne wollen wir uns einige Teile dieser Infrastruktur näher ansehen.  Dabei wird sich herausstellen, dass korrekte Implementierungen der Infrastruktur keineswegs immer trivial sind.  Was theoretisch so harmlos aussieht, kann in der Praxis tückisch sein.  Landläufig herrscht die Meinung: "Es ist doch kein Problem, clone() oder equals() zu implementieren.  Da muss man doch nur alle Felder kopieren bzw. miteinander vergleichen und das war's dann schon. Oder nicht?"  Oder doch? Wir werden sehen!

Schauen wir uns diesmal den Objektvergleich mittels equals() an.
 
 

Objektvergleich in Java


In Java gibt es zwei Möglichkeiten, Variablen zu vergleichen: die eine ist der Vergleich über den == Operator, die andere Möglichkeit ist der Vergleich mit Hilfe der equals()-Methode.

Beispiel:

  int x = 100;
  int y = 100;
  ...
  if (x==y) ...

Hier werden zwei int-Variablen miteinander verglichen.  Für den Vergleich gibt es nur den == Operator, weil der Typ int keine equals()-Methode hat.  Generell unterscheidet man in Java zwischen Variablen vom primitivem Typ und Referenz-Variablen.

Primitive Typen sind in der Sprache vordefinierte Typen wie int, long, boolean, etc..  Für Variablen vom primitivem Typ gibt es nur den Vergleich über den == Operator und der liefert true, wenn beide Variablen den gleichen Wert enthalten, wie das in obigem Beispiel der Fall ist.

Nicht-primitiven Typen sind Klassen und Interfaces.  Alle Variablen diesen Typs sind in Java Referenzvariablen.  Sie verweisen lediglich auf  Objekte, enthalten diese Objekte aber nicht.

Beispiel:

  String s1 = new String("Hello World !");
  String s2 = new String("Hello World !");
  ...
  if (s1 == s2) ...            // yields false
  ...
  if (s1.equals(s2)) ...       // yields true

Hier werden zwei String-Variablen verglichen. String ist eine Klasse und deshalb sind die beiden Variablen s1 und s2 Referenzvariablen. Für Referenzvariablen gibt es neben dem Vergleich per == Operator den Vergleich mit Hilfe der equals()-Methode.  Die beiden Vergleiche haben nicht nur unterschiedliche Syntax, sondern auch unterschiedliche Semantik.
Der Vergleich per == Operator ist die Prüfung auf Identität der beiden referenzierten Objekte. In unserem Beispiel haben wir zwei Referenzen s1 und s2 auf zwei String -Objekte, die an verschiedenen Stellen auf dem Heap angelegt wurden und den gleichen Inhalt haben.  Die beiden referenzierten String-Objekte sind zwar gleich in dem Sinne, dass sie den gleichen Inhalt, nämlich "Hello World !", haben, aber sie sind nicht identisch, da sie an verschiedenen Stellen im Speicher angelegt sind.
Das Beispiel zeigt den Unterschied zwischen dem == Operator und der equals()-Methode: Der Vergleich mittels == Operator prüft auf Identität der referenzierten Objekte, während der Vergleich mittels equals()-Methode im Falle von String auf Gleichheit des Inhalts der referenzierten Objekte prüft.  In unserem Beispiel liefert der erste Vergleich false (d.h. "nicht identisch") und der zweite Vergleich true (d.h. "inhaltlich gleich").

Damit haben wir nun ein erstes intuitives Verständnis von equals():  es prüft auf inhaltliche Gleichheit im Gegensatz zum == Operator, der auf Identität prüft (equality vs. identity).

Leider ist es nicht immer so, dass equals() und der == Operator diese unterschiedlichen Eigenschaften haben. Man findet schon in den Java-Bibliotheksklassen Beispiele für abweichendes Verhalten.

Beispiel:

  String init = "Hello World !";

  StringBuffer sb1 = new StringBuffer(init);
  StringBuffer sb2 = new StringBuffer(init);
  ...
  if (sb1 == sb2) ...            // yields false
  ...
  if (sb1.equals(sb2)) ...       // yields false (!!!)

Offenbar sind StringBuffer-Objekte selbst bei gleichem Inhalt nicht gleich; jedenfalls ist dies das Ergebnis des Vergleichs mittels equals().  Wie kann das sein?

Nun, das liegt daran, dass jede Klasse die equals()-Methode von der Superklasse Object erbt.  Eine Klasse wie StringBuffer, die die geerbte equals()-Methode nicht überschreibt, stellt damit automatisch die Default-Implementierung von equals()aus Object zur Verfügung.  Die Default-Implementierung ist aber identisch mit dem Verhalten des == Operators: es wird auf Identität der referenzierten Objekte geprüft.

Dieses Defaultverhalten von equals()aus Object erklärt sich dadurch, dass in der Klasse Object über die Struktur und den Inhalt von Subklassen nichts bekannt ist.  Eine universelle Implementierung von equals(), die für jede beliebige Subklasse "das Richtige" tut, nämlich den Inhalt vergleichen, wäre zwar machbar gewesen (mit Hilfe von dynamischer Typinformation), aber aufwendig.  Die Designer der Klasse Object haben sich für eine einfachere Lösung entschieden und deshalb wird in Object.equals() nur auf Identität und nicht auf inhaltliche Gleichheit geprüft.

Dieses Default-Verhalten von Object.equals() und die Tatsache, dass die Klasse StringBuffer die geerbte equals()-Methode nicht überschreibt, erklären, warum in obigem Beispiel in beiden Vergleichen false als Ergebnis geliefert wird: die StringBuffer-Objekte haben zwar gleichen Inhalt, sind aber nicht identisch.

Ob das Ergebnis des Vergleichs von StringBuffer-Objekten mittels equals() das ist, was man erwartet, kann man sicher kontrovers diskutieren.  Zumindest wirft es Fragen auf ...  wann muss eine Klasse die Default-Implementierung von equals() überschreiben, und wann nicht?  Und wenn ja, wie?  Damit wollen wir uns im Folgenden beschäftigen.
 
 

Value vs. Entity-Types

Typen lassen sich in zwei Kategorien einteilen: man unterscheidet zwischen sogenannten Value- und Entity-Typen.
  • Value-Typen . Alle primitiven Typen in Java sind Value-Typen. Sie enthalten einen Wert und dieser Wert ist das Wesentliche.  Klassen können ebenfalls Value-Typen sein.  Bei solchen Klassen ist der Inhalt der Objekte ganz wesentlich.  Der Inhalt repräsentiert den Wert des Objekts und bestimmt das Verhalten der Objekte fast vollständig.  Beispiele solcher Value-Klassen sind die Standard-Klassen BigDecimal, String, Date, Point, etc.
  • Entity-Typen . Darunter versteht man Klassen, bei denen der Inhalt nicht das Wesentliche ist.  Sie werden nicht als "Werte" betrachten und auch nicht als "Wert" herumgereicht.  Das sind Typen, die hauptsächlich Dienste anbieten, oder Typen, die Referenzen auf andere unterliegende Objekte darstellen. Beispiele sind die Standardklassen Thread, Socket, oder FileOutputStream.
Betrachten wir zur Illustration ein Thread-Objekt und ein String-Objekt.  Ein String-Objekt ist im wesentlichen durch seinen Inhalt, nämlich die enthaltene Zeichenkette, bestimmt.  Davon kann man Kopien anlegen und man kann sie vergleichen. Das ist bei einem Thread-Objekt ganz anders. Natürlich hat auch ein Thread -Objekt Inhalt; ein Thread hat einen Namen und einen Zustand (runnable, blocked, dead, usw.) und eine Priorität und er verwendet ein Runnable-Objekt, dessen Code er ausführt.  Aber all diese Eigenschaften ergeben in ihrer Gesamtheit keinen "Wert", den man vergleichen oder kopieren möchte. Wann sind zwei Threads gleich?  Wenn sie denselben Namen haben? Oder denselben Code ausführen? Das macht logisch keinen Sinn. Was soll man sich  unter der Kopie eines Threads vorstellen? Auch das macht nicht so recht Sinn. In solchen Fällen spricht man von Entity-Typen, wobei die Grenze zwischen Value- und Entity-Typen oftmals schwer zu ziehen ist.

Was bedeutet die Unterscheidung zwischen Value- und Entity-Typen für die Implementierung von equals()?

Entity-Typen überschreiben selten die equals()-Methode.  Da sie keine Werte darstellen, ist der Vergleich des Inhalts praktisch bedeutungslos und aus diesem Grunde ist es völlig in Ordnung, wenn zwei Entity-Objekte genau dann "gleich" sind, wenn sie identisch sind.

Das ist bei Value-Typen ganz anders.  Der Inhalt ist das Wesentliche des Objekte und deshalb sind zwei Value-Objekte genau dann gleich, wenn sie den gleichen Inhalt haben.  In solchen Fällen muss equals() überschrieben werden, denn die Default-Implementierung ist unbrauchbar für solche Value-Typen.

Was schließen wir daraus?  Eine der ersten Entscheidungen, die beim  Design einer neuen Klasse gefällt werden muss, ist die Entscheidung, ob die Klasse Value- oder Entity-Objekte beschreiben soll.  Im Falle von Entity-Verhalten kann man sich die Arbeit mit equals() sparen; im Falle von Value-Verhalten muss man es implementieren.
 
 

In der Praxis

Wie ist das nun in der Praxis?

"Habe ich was falsch gemacht, wenn ich eine Klasse ohne equals() geschrieben habe?"

Das kommt darauf an.  Wenn es ein Entity-Typ ist, also eine reine Service-Klasse ist oder einen Verweis auf irgendwas darstellt, dann nicht.  Wenn es aber ein Value-Typ ist, dann ist die geerbte equals()-Methode normalerweise inkorrekt.

"Aber ich weiß genau, dass equals() überhaupt nicht aufgerufen, nirgendwo in der gesamten Applikation.  Wozu soll ich mir all die ganze Arbeit machen, wenn das sowieso keiner braucht?"

Das ist eine überzeugendes Argument!  Aber ... wer kann schon mit Bestimmtheit sagen, dass eine Methode, die heute nicht gebraucht wird, morgen ebenfalls nicht gebraucht werden wird?  Das Gefährliche an equals() ist, dass es immer definiert ist, weil es bereits in der Superklasse Object implementiert ist.  Wenn morgen jemand MyClass.equals() ruft, dann lässt sich das klaglos übersetzen und es läuft ... aber leider falsch.  Die dann einsetzende Fehlersuche erinnert fatal an die Suche nach Pointer-Problemen in C oder C++ - und das glaubte man doch in Java hinter sich gelassen zu haben.  Sobald man sich halbwegs darüber klar geworden ist, dass man mit seiner Klasse einen Value-Typen implementiert, dann sollte man auf jeden Fall equals() korrekt implementieren.  Alles andere ist fahrlässig.

Erschwerend kommt hinzu, dass equals() nicht immer sichtbar benutzt wird, sondern bereits implizit von gewissen JDK-Klassen verwendet wird.  Der wichtigste Vertreter dieser equals()-benutzenden JDK-Klassen sind die hash-basierten Container wie Hashtable, HashMap und HashSet.  Aber auch andere Klassen benutzen equals().  Häufig ist dies nicht einmal explizit in der JavaDoc ausgewiesen; eine korrekte equals()-Implementierung wird deshalb von jeder Klasse erwartet.  Man kann also gar nicht mit Gewissheit sagen, dass equals() nicht gebraucht wird, weil es nicht benutzt wird.

Das heißt, der Autor einer Klasse muss in jedem Fall entscheiden, welche Semantik (Entity- oder Value-Typ) die Klasse haben soll. Daraus ergibt sich dann die Semantik für die equals()-Methode der neuen Klasse. Anders als bei anderen Teilen der Objekt-Infrastruktur kann man sich bei equals() um die Entscheidung nicht drücken.  Wenn man sich nicht entscheidet, ist die Klasse mit ihrer geerbten Default-Implementierung von equals() u.U. inkorrekt.
 
 

Der sogenannte equals()-Contract

Wenn man nun equals() implementieren will, was muss man tun?  Was wird von equals() erwartet?  Intuitiv ist klar, dass es den Inhalt zweier Objekte vergleichen soll. Aber was bedeutet das genau?

Der Vergleich zweier Objekte sollte gewissen Regeln folgen, die man mehr oder weniger intuitiv von einem Vergleich erwartet.  Diese zusätzlichen Eigenschaften einer Implementierung von equals() sind formal beschrieben im sogenannten "equals()-Contract".  Den equals()-Contract findet man in der JDK JavaDoc unter Object.equals().  Hier ist die Originalbeschreibung aus der API Spezifikation der JavaTM 2 Platform, Standard Edition:

     public boolean equals(Object obj)

          Indicates whether some other object is "equal to" this one.

          The equals method implements an equivalence relation:

    • It is reflexive: for any reference value x, x.equals(x) should return true.
    • It is symmetric: for any reference values x and y, x.equals(y) should return true if and only if y.equals(x) returns true.
    • It is transitive: for any reference values x, y, and z, if x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should return true.
    • It is consistent: for any reference values x and y, multiple invocations of x.equals(y) consistently return true or consistently return false, provided no information used in equals comparisons on the object is modified.
    • For any non-null reference value x, x.equals(null) should return false.


Das bedeutet das Folgende:

  • 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.


Eigentlich sind die Forderungen im equals()-Contract naheliegend und intuitiv verständlich.  Das ist genau das, was jeder von einer Gleichheitsrelation erwartet.  Man sollte also 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.
 
 

Anleitung zum Implementieren von equals()

Im Folgenden werden wir equals() Zeile für Zeile implementieren.

Signatur

Eine Implementierung von equals() beginnt damit, dass man sich die Signatur (d.h. Anzahl und Type der Argumente) der equals()-Methode überlegen muss.  Üblicherweise will man die Version von equals(), die in der Klasse Object definiert ist, überschreiben.  Aus diesem Grunde ist es klar, dass die equals()-Methode der eigenen Klasse exakt dieselbe Signatur haben muss, wie Object.equals(), nämlich

  public boolean equals(Object other)

Es gibt alternative Ansätze, bei denen Überschreiben und Überladen kombiniert wird, aber diese Technik ist ungewöhnlich und wir wollen sie deshalb zunächst nicht betrachten.
 

Alias-Prüfung

Man kann die Implementierung von equals() sofort beenden, wenn die beiden zu vergleichenden Objekte identisch sind.  In diesem Falle müssen sie den gleichen Inhalt haben und man kann sich den gesamten Vergleich des Inhalts sparen.  Aus Optimierungsgründen kann man daher als erstes auf Identität von this und other prüfen.

  public boolean equals(Object other) {
    if (this == other)
       return true;
    ...
  }

Aufgabenteilung in einer Klassenhierarchie

Die nächsten Schritte der Implementierung sind abhängig davon, ob die Klasse, deren equals()-Methode wir implementieren wollen, eine direkte Subklasse von Object ist, oder ob es sich um eine in der Klassenhierarchie weiter unten liegende Subklasse handelt.  Wir unterscheiden daher im Folgenden zwischen direkten Subklassen von Object und indirekten Subklassen von Object. (Eigentlich muss man den Begriff "direkte Subklasse von Object" noch etwas präziser fassen.  Als "direkte Subklasse von Object" betrachten wir hier die erste Subklasse, die equals() implementiert.  Es kann also durchaus vorkommen, dass Object eine Subklasse hat, die aber kein equals() implementiert, beispielsweise weil sie keine Felder hat.  Wenn es eine Sub-Subklasse gibt, die equals() implementiert, dann ist diese Sub-Subklasse die "direkte Subklasse von Object" im Sinne unserer Definition.) Die Unterscheidung zwischen direkten und indirekten Subklassen ist bedeutsam, weil gewisse Aufgaben in einer Klassenhierarchie nur einmal erledigt werden müssen. Und diese Aufgaben werden von direkten Subklassen übernommen.

Generell ist es so, dass eine Subklasse den Vergleich ihrer Felder durchführt und den Vergleich der von den Superklassen geerbten Felder an ihre direkte Superklasse delegiert.  Ähnlich wie bei Konstruktoren delegiert dabei jede Klasse an ihre direkte Superklasse, so dass rekursiv die equals()-Methoden der gesamten Hierarchie aufgerufen werden und damit das gesamte Objekt verglichen wird.  Die Rekursion endet bei der Klasse in der Hierarchie, die direkt von Object abgeleitet ist.  Sie ruft die equals()-Methode ihrer Superklasse, nämlich Object.equals(), nicht auf.  Statt dessen übernimmt sie gewisse Sonderaufgaben.

Dazu gehört die Prüfung auf Vergleichbarkeit, d.h. die Prüfung, ob der Inhalt von this überhaupt mit dem Inhalt von  other verglichen werden kann. [1] Der Vergleich ist beispielsweise nicht möglich, wenn other eine null-Referenz ist, also keinen Inhalt hat. Der Vergleich ist auch dann nicht möglich, wenn other auf ein Objekt verweist, dass von einem gänzlich inkompatiblen Typ ist.  Schließlich lassen sich "Äpfel und Birnen" nicht miteinander vergleichen.  Hingegen ist der Vergleich immer dann möglich, wenn this und other von genau dem gleichen Typ sind.
 

Indirekte Subklassen von Object

Sehen wir uns zunächst die Klassen an, die nicht direkt von Object abgeleitet werden.  In der equals()-Implementierung solcher Klassen wird nach dem bereits gezeigten Alias-Check an die Superklasse delegiert und super.equals() gerufen.

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

Direkte Subklassen von Object

In einer Klasse, die direkt von Object abgeleitet ist, wird nicht super.equals() aufgerufen.  Statt dessen wird der Fall einer null-Referenz behandelt und auf Vergleichbarkeit geprüft.

Der Aufruf von super.equals() ist nicht nur überflüssig, sondern wäre ernsthaft falsch; man sollte ihn also nicht etwa versehentlich machen. super.equals() ist im Falle einer direkten Subklasse von Object genau Object.equals().  Die Implementierung von Object.equals()liefert aber auch false, wenn this und other zwar denselben Inhalt haben, aber als Duplikate im Speicher an verschiedenen Stellen angelegt sind, also nicht identisch sind.  Die Information, die Object.equals()liefert ist daher unbrauchbar für die Implementierung der equals()-Methode einer Subklasse.

Zu den Sonderaufgaben einer direkten Subklasse von Object:

Test auf null

Nach dem Alias-Check wird geprüft, ob other eine null-Referenz ist.

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

Diesen Test kann man im Prinzip auch in jeder indirekten Subklasse machen, aber es genügt, ihn genau einmal in der obersten Klasse durchzuführen.  Wenn man sich an das Koch-Rezept hält, d.h. wenn jede Subklasse nach dem Alias-Check als erstes an die Superklasse delegiert und die oberste Klasse als erstes auf null abprüft, dann erfolgt die Prüfung garantiert, bevor irgendwelche Zugriffe auf Felder von other erfolgen.  Damit ist sichergestellt, dass es nicht zu einer NullPointerException kommt, denn diese würde den equals()-Contract verletzen.  Der equals()-Contract verlangt, dass der Vergleich mit null-Referenzen das Ergebnis false liefert; das Werfen einer NullPointerException ist daher kein konformes Verhalten.

Ganz allgemein sollte man es vermeiden, equals() mit einer NullPointerException zu beenden.  Als Ergebnis von equals() wird true oder false erwartet.  Wenn der Vergleich aus irgendwelchen Gründen nicht gemacht werden kann, dann sollte keine Exception geworfen werden, sonders es sollte false als Ergebnis geliefert werden.

Test auf Vergleichbarkeit

Nachdem getestet ist, dass other keine null-Referenz ist, wird auf Vergleichbarkeit geprüft.  Interessanterweise ist das das kontroversesten Themen im Zusammenhang mit equals() überhaupt.  Wir werden den Test auf Vergleichbarkeit in der nächsten Kolumne noch näher beleuchten.  Hier nur ein erster Einblick in die Problematik.

Der Vergleichbarkeitstest ist nötig, weil equals() ein Argument vom Typ "Referenz auf Object" akzeptiert. Eine solche Referenz kann daher auf jede Art von Objekt zeigen und es ist keineswegs sicher gestellt, dass other auf ein Objekt desselben Typs wie this zeigt oder dass die referenzierten Objekte wenigstens in irgendeiner Form vergleichbar sind.  Die Vergleichbarkeit muss daher durch einen expliziten Test feststellen.

Das heißt, man muss sich vor der Implementierung von equals(), genau genommen schon beim Design der Klasse, überlegen, mit welcher Art von Objekten ein Vergleich überhaupt möglich und sinnvoll ist.  Im einfachsten Fall ist der Vergleich nur zwischen Objekten gleichen Typs erlaubt.  Das sieht dann wie folgt aus:

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

Daneben gibt es zahlreiche Techniken, bei denen versucht wird, den Vergleich zwischen Sub- und Superobjekten zu erlauben.  Immerhin haben Sub- und Superobjekte einen gemeinsamen Superklassenanteil, den man miteinander vergleichen kann.  Solche Implementierungen sind aber meistens inkorrekt, weil sie nicht transitiv und oft nicht einmal symmetrisch sind, und damit nicht den Anforderungen aus dem equals()-Contract entsprechen.  Diese häufig fragwürdigen Implementierungen sind leider so populär, dass wir ihnen die nächste Ausgabe der Kolumne widmen werden.
 

Vergleich der Felder

Nach dem Delegieren an die Superklasse bzw. den Tests auf null und auf Vergleichbarkeit folgt in direkten wie indirekten Subklassen der eigentliche Vergleich der Felder.

Im Prinzip müssen alle Felder von this mit den korrespondierenden Feldern von other verglichen werden.  Wie das im einzelnen geschieht, hängt vom Typ der Felder ab.  Man unterscheidet zwischen

  • transienten Feldern,
  • Feldern von primitivem Typ,
  • Feldern, die Referenzen sind und den Wert null haben können,
  • Feldern, die Referenzen auf Objekte sind die keine korrekte Implementierung von equals() haben, und
  • allen übrigen Feldern.

Transiente Felder

Transiente Felder werden ignoriert.  Sie tragen nichts zum Zustand des Objekts bei und gehören logisch nicht zum Inhalt des Objekts. Daher werden sie beim Vergleich nicht berücksichtigt.

Primitive Typen

Felder von primitivem Typ werden mit Hilfe des == Operators verglichen.  Beispiel:

  class MyClass {
   private int size;
   ...
   public boolean equals(Object other) {
    ...
    if (size != ((MyClass)other).size)
       return false;
    ...
   }
  }

Bei primitiven Typen vergleicht der == Operator den Inhalt und das ist genau das, was wir hier brauchen.

Referenz-Typen

Felder, die Referenzen sind, werden verglichen, indem die equals()-Methoden der referenzierten Objekte gerufen werden.  Beispiel:

  class MyClass {
   private String s;
   ...
   public boolean equals(Object other) {
    ...
   if (!(s.equals(((MyClass)other).s)))
      return false;
    ...
   }
  }

Mögliche null-Referenzen

Obige Lösung ist natürlich nur korrekt, wenn von der Logik der Klasse her sichergestellt ist, dass das betreffende Feld keine null-Referenz sein kann.  Wenn null ein möglicher Wert ist, dann muss zuvor auf null abgeprüft werden, um eine NullPointerException zu verhindern.  Beispiel:

  class MyClass {
   private String possNull;
   ...
   public boolean equals(Object other) {
    ...
    if (possNull == null)
    {if (((MyClass)other).possNull != null)
          return false;
    }
    else
    {if (!(possNull.equals(((MyClass)other).possNull)))
          return false;
    }
    ...
   }
  }

Typen ohne korrektes equals()

Der Aufruf der equals()-Methode des referenzierten Objekts macht nur Sinn, wenn diese equals()-Methode korrekt, d.h. konform zu den Regeln des equals()-Contracts, implementiert ist.  Andernfalls muss man Sonderlösungen und Umgehungen finden.

Ein Beispiel für einen solchen Typ, der eine Sonderbehandlung braucht, ist StringBuffer. Man kann StringBuffer-Objekte zwar per StringBuffer.equals() miteinander vergleichen, aber es wird auf Identität und nicht auf inhaltliche Gleichheit geprüft.  Das ist nicht das, was hier gebraucht wird; wir wollen den Inhalt der StringBuffer-Objekte vergleichen und brauchen daher eine Umgehungslösung, die man mit Hilfe der Klasse String bauen kann.  Für einen korrekten Vergleich des Inhalts von zwei StringBuffer-Objekten kann man beide Objekte in String-Objekte konvertieren (per toString()-Methode) und dann die String-Objekte per String.equals() miteinander vergleichten

Ein anderes Beispiel für Felder, die eine Sonderbehandlung brauchen, sind Arrays. Zum Vergleich von zwei Arrays muss der Inhalt der Arrays elementweise verglichen werden, indem man für jedes Array-Element die equals()-Methode aufruft (oder bei Elementen von primitivem Typ den == Operator).  Zur Arbeitserleichterung gibt bereits eine Hilfsklasse, nämlich java.util.Arrays. Diese Klasse hat eine statische Methode equals(), die genau das oben beschriebene tut.

Am Ende, wenn alle Prüfungen und Vergleiche erfolgreich durchgeführt worden sind, wird true zurückgegeben.
 

Rollenspiele

Klassen ohne korrekte Implementierung von equals() machen ihren Benutzern reichlich Probleme.  Die Probleme sind insbesondere dann gravierend, wenn es sich bei der inkorrekten Klasse um eine Superklasse handelt.  Die Subklasse hat dann kaum noch eine Chance, ihrerseits eine korrekte Implementierung von equals() zur Verfügung zu stellen, weil Zugriff auf die privaten Daten der Superklasse gar nicht möglich ist und u.U. die Kette von rekursiven Delegationen an super.equals() unterbrochen ist.  Es ist daher bei der Implementierung von potentiellen Superklassen, d.h. von Klassen, die nicht als final deklariert sind, besonders wichtig, dass sie equals() korrekt implementieren und nach Möglichkeit dem vorgeschlagenen Rezept folgend ihre Sonderaufgaben übernehmen. [2]

Hier nochmals in der Übersicht die Verantwortlichkeiten in einer Klassenhierarchie, sowie Beispielimplementierungen im Source-Code:

Implementierung von equals() in einer direkten Subklasse von Object


  class MyClass {
   private String s;
   private int i;
   ...
   public boolean equals(Object other) {
   if (this == other)
      return true;
   if (other == null)
      return false;
   if (other.getClass() != getClass())
      return false;

   if (!(s.equals(((MyClass)other).s)))
      return false;
   if (i != ((MyClass)other).i)
      return false;
   ...
   return true;
   }
  }
 

Implementierung von equals() in einer indirekten Subklasse von Object

  class MySubclass extends MyClass {
   private String t;
   ...
   public boolean equals(Object other) {
   if (this == other)
      return true;
   if (!super.equals(other))
      return false;

   if (!(t.equals(((MySubClass)other).t)))
      return false;
   ...
   return true;
   }
  }

Haben wir damit den equals()-Contract erfüllt? Sehen wir uns die 5 Anforderungen noch einmal an.

  1. Jedes Objekt liefert beim Vergleich mit sich selbst true.

  2. Das ist erfüllt, weil wir als erstes den Alias-Check ausführen.
  3. Es ist egal, ob man x mit y vergleicht, oder y mit x; das Ergebnis ist dasselbe.

  4. Wir vergleichen nur Objekte gleichen Typs miteinander; deshalb ist unsere Implementierung von equals() symmetrisch.
    Verletzungen dieser Forderung können auftreten, wenn der Vergleich zwischen Objekten unterschiedlichen Typs erlaubt wird. Dann findet man bisweilen Implementierungen, die asymmetrisch sind, weil A.equals() den Vergleich mit Objekten des Typs B erlaubt, aber B.equals() den Vergleich mit A-Objekten nicht zulässt.  Dazu mehr im nächsten Artikel.
  5. Wenn x gleich y ist und y gleich z, dann sind auch x und z gleich.

  6. Wir vergleichen nur Objekte gleichen Typs miteinander; deshalb ist unsere Implementierung von equals() transitiv.
    Verletzungen dieser Forderung können auftreten, wenn der Vergleich zwischen Objekten unterschiedlichen Typs erlaubt wird. Dann findet man bisweilen Implementierungen, die intransitiv sind, weil der Vergleich zwischen einem  A-Objekt und einem B-Objekt true liefern kann und genauso der Vergleich zwischen dem B-Objekt und einem anderen A-Objekt; aber das heißt noch nicht, dass deshalb die beiden A-Objekte gleich sind, obwohl das laut equals()-Contract so sein sollte.  Dazu mehr im nächsten Artikel.
  7. Man kann zwei Objekte beliebig oft miteinander vergleichen; es kommt immer dasselbe heraus, solange sich die Objekte nicht verändern.

  8. In unserer Implementierung von equals() werden die Felder miteinander verglichen. Es geht keine weitere Information in die Produktion des Booleschen Ergebnisses ein.  Deshalb ist das Ergebnis immer dasselbe, sofern sich die Objekte nicht ändern.
    Fehler können nur auftreten, wenn beispielsweise statische Daten in die Ermittlung des Ergebnisses eingehen würden, was aber ganz ungewöhnlich wäre.
  9. Alle Objekte sind von null verschieden.

  10. Das haben wir durch die Prüfung auf null in der direkten Basisklasse von Object erreicht und dadurch, dass wir NullPointerExceptions sorgfältig vermieden haben.

Zusammenfassung und Ausblick

Jede Klasse in Java muss ein Minimum an Objekt-Infrastruktur implementieren, damit sie sinnvoll verwendbar ist.  Zu diesen grundlegenden Methoden gehört u.a. auch die equals()-Methode.  Dabei ist man nicht frei in der Wahl der Semantik, die man der equals()-Methode gibt.  Jede Implementierung von equals()sollte die Regeln des sogenannten equals()-Contracts befolgen.  Dafür ist es nötig, dass Klassen, die Value-Typen repräsentieren, die Defaultimplementierung aus Object.equals() überschreiben, weil diese auf Identität und nicht auf inhaltliche Gleichheit prüft, was für Value-Typen semantisch falsch ist. Wir haben das Prinzip solcher Implementierungen diskutiert, wobei eine Reihe von Details offen geblieben sind.  Insbesondere ist offen, welche Arten von Objekten als "miteinander vergleichbar "gelten sollen.  Wir haben nur den Vergleich von Objekten gleichen Typs erlaubt.  Dazu gibt es aber Alternativen, die wir im nächsten Artikel diskutieren werden.
 

Fußnoten

[1] Dieser Vergleich ist nicht zu verwechseln mit dem Alias-Check, bei dem geprüft wird, ob this und other identisch sind.

[2] Andere Vorgehensweisen sind denkbar, aber in jedem Falle sollten alle Klassen in einem Projekt oder zumindest in einer Klassenhierarchie demselben Konzept folgen.  Alternative Implementierungen von equals() und deren Vor- und Nachteile besprechen wir im nächsten Artikel.
 

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.Equals.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
/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
/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/JavaSolutions/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/JavaSolutions/SecretsOfEquals/Equals-2.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

 
 

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-2012 by Angelika Langer.  All Rights Reserved.    URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/01.Equals-Part1/01.Equals1.html  last update: 1 Nov 2012