Das Problem - Was ist eine Identität?
Das Beispiel in Abbildung 1 zeigt ein typisches Szenario, in dem die Identität von Objekten eine Rolle spielt.
| ||
| ||
| ||
|
Der Code erscheint zunächst recht simpel: Es sollen alle Geldtransaktionen, die auf ein Konto gehen, zu einer Summenbuchung zusammengefasst werden. Dazu werden in den Zeilen 03 - 06 die Geldtransaktionen als Transaction-Objekte erzeugt. Die Transaktion setzt sich aus der Angabe zu dem Konto (Bankleitzahl und Kontonummer) und dem Betrag der Transaktion zusammen.
Zur Bildung der Summenbuchungen wird eine HashMap befüllt (Zeilen 10 - 20). Der Schlüsselwert ist das Konto. Der Wert enthält die Summe der Beträge aus den Überweisungen.
Doch das gewünschte Ergebnis bleibt aus, denn in Zeile 11 passiert das Unerwartete: summenbuchung.get(...) gibt stets null zurück, obwohl doch die ersten drei Transaktionen mit dem gleichen Konto durchgeführt werden. summenbuchung.get(...) vergleicht das Konto im Argument mit den bereits vorhandenen Konten in summenbuchung. Und wer entscheidet, ob zwei Konten übereinstimmen? Die Klasse Konto. Sie muss es wohl wissen. Doch woher weiß sie das?
Genau genommen benutzt HashMap die Methode equals von Konto, um zu entscheiden, ob zwei Konten identisch sind. Für das obige Beispiel ist diese Methode nicht überschrieben worden - so erbt Konto die Standardimplementierung von Object. Die besagt, dass zwei Objekte nur dann gleich sind, wenn es sich um ein und dieselbe Instanz handelt (object1 == object2).
So konsequent diese Entscheidung auch sein mag, sie ist wenig hilfreich bei der Umsetzung der folgenden Geschäftslogik.
Die equals-Methode - gleich oder nicht gleich?
Zwei Konten sollen dann gleich sein, wenn Kontonummer und Bankleitzahl übereinstimmen. Diese Logik lässt sich durch das Überschreiben der Methode equals implementieren.
Die Implementierung der Methode equals wird fast immer aussehen wie in Abbildung 2. Von besonderer Bedeutung sind die ersten Zeilen; am wichtigsten ist die Signatur. Die Methode muss boolean zurückgeben, das Argument ist vom Typ Object. Stimmt die Signatur nicht, wird die Methode nicht für den Vergleich benutzt.
In einem ersten Schritt (siehe Zeile 02) wird entschieden, dass zwei Objekte nicht gleich sein können, wenn ihr Typ nicht übereinstimmt. Die Überprüfung erledigt der Operator instanceof. Dieser berücksichtigt auch den Fall o == null. Als Ergebnis liefert instanceof dann immer false.
Ist das Argument vom richtigen Typ, können die Attribute verglichen werden, die für eine Gleichheit übereinstimmen müssen. Im Beispiel sind dies die Attribute kontonummer und bankleitzahl, die beide vom primitiven Datentyp long sind und daher über den Gleichheitsoperator verglichen werden können.
Verträge sind einzuhalten
Es lohnt sich, einen Blick in die Java-Dokumentation zur Methode Object.equals zu werfen, um zu erfahren, was von der Methode erwartet wird. Dieser Vertrag ist für den Entwickler bindend:
- Reflexivität: Stimmen die beiden Objektinstanzen überein, so muss equals stets true liefern.
- Symmetrie: Stellt die Vertauschbarkeit dar. Wenn a.equals(b) true ergibt, so muss auch b.equals(a) true ergeben.
- Transitivität: Gilt a.equals(b) == true && b.equals(c) == true, so muss stets auch a.equals(c) zutreffen.
- Konsistenz: Das Ergebnis von a.equals(b) muss reproduzierbar sein (sofern a und b sich nicht verändern).
- Null ist niemals gleich mit einem Objekt: a.equals(null) muss immer false ergeben.
Damit ist der technische Aspekt der Methode beschrieben. Abweichungen von diesen Regeln sind niemals sinnvoll und führen zu schwer nachvollziehbaren Effekten.
Was ist die Identität eines Objekts?
Eine Herausforderung ist die fachliche Definition: Wann sind zwei Objekte gleich? Welche Attribute müssen übereinstimmen?
Leitfaden dieser Überlegung ist: Was macht die Identität eines Objekts aus? In Wikipedia [1] wird der Begriff Identität erläutert: "[...] In einem weiteren (sozial)psychologischen Sinne versteht man unter Identität häufig die Summe der Merkmale, anhand derer sich ein Individuum von anderen unterscheiden lässt: Das erlaubt eine eindeutige Identifizierung. [...]"
Das, was für den Menschen gilt, ist auch für Objekte zutreffend: Die Identität eines Objekts wird durch seine Attribute feststellbar. In der Regel wird gefordert, dass die Identität unabhängig von der Zeit gültig ist. Daraus folgt, dass die Identität durch unveränderbare Attribute gebildet wird.
Diese Attribute werden im Idealfall syntaktisch als final gekennzeichnet. Ist dies nicht möglich, so müssen andere Maßnahmen zur Erhaltung der Identität getroffen werden.
In jedem Fall gilt: Lässt sich die Identität von Objekten verändern, so sind diese nicht als Schlüsselwerte einer Hash-Collection geeignet.
Wer equals sagt, muss auch hashCode definieren
Zurück zum Ausgangsproblem: Mit der Implementierung von equals ist klar, wann zwei Konten als gleich anzusehen sind. Dennoch liefert Abbildung 1 noch immer nicht das gewünschte Ergebnis.
summenbuchung.get(...) in Zeile 11 verwendet neben equals auch die Methode hashCode von Konto. Das Wort Hash im Namen der Klasse HashMap erinnert den Eingeweihten an die Methode hashCode.
Für einen schnellen Zugriff auf die Schlüsselwerte legt HashMap diese indexiert ab (siehe Abbildung 3).
Der Index ist eine Integer-Zahl und wird aus der Methode hashCode der Schlüsselobjekte gebildet. Objekte mit demselben Hashcode werden gemeinsam in so genannten Buckets gespeichert. Buckets sind Listen von Objekten mit demselben Hashcode.
HashMap.get findet das Schlüsselobjekt mit Hilfe dieser Datenorganisation wie folgt:
- Es wird der Hashcode des Arguments von get ermittelt.
- Es wird geprüft, ob es einen Bucket mit dem oben ermittelten Hashcode gibt. Ist das nicht der Fall, so gibt get das Ergebnis null zurück.
- Es werden alle Elemente des Buckets mit dem Argument von get über die Methode equals verglichen.
- Das erste Element des Buckets, für das equals true zurückgibt, wird als gleicher Schlüssel identifiziert. Der zugehörige Wert wird zurückgeliefert.
Forderungen an hashCode
Aus dieser Suchstrategie ergeben sich unmittelbar die folgenden Forderungen an die Implementierung der Methode hashCode:
- Konsistenz: hashCode muss immer denselben Wert ergeben. Andernfalls würde HashMap ein einmal abgelegtes Objekt nicht mehr finden.
- Sind zwei Objekte im Sinne von equals gleich, so müssen sie auch in ihrem Hashcode übereinstimmen. Da zuerst über den Hashcode gesucht wird, werden sonst gleiche Objekte nicht gefunden.
Daraus ergibt sich, dass nur die Attribute zur Bildung des Hashcodes herangezogen werden können, deren Übereinstimmung auch bei equals überprüft wird. Die Implementierungen von equals und hashCode müssen aufeinander abgestimmt sein.
Bezüglich der Ausführungsgeschwindigkeit ist zu beachten: Die Ermittlung des Hashcodes sollte einerseits schnell sein. Andererseits sollen möglichst wenig Objekte in einem Bucket liegen. Im Idealfall ist bereits der Hashcode eindeutig. In diesem Fall muss HashMap nur einmal die Methode equals ausführen, um die Identität festzustellen.
Praxistaugliche Implementierung
Die Erfüllung aller Anforderungen an hashCode ist nicht trivial. Im Folgenden wird eine Vorgehensweise vorgestellt, die sicher nicht optimal, im Alltag aber meist brauchbar ist. Sie ist angelehnt an den Klassiker von Joshua Block "Effective Java, Programming Language Guide", Addison-Wesley, 2003.
Zu jedem Attribut, das in equals auf Übereinstimmung geprüft wird, wird der Hashcode in folgender Weise ermittelt:
Für einfache Datentypen werden die hashCode-Methoden der Wrapper-Klassen verwendet. Für die übrigen Attribute sind die (sinnvoll definierten) hashCode-Methoden zu verwenden. Abbildung 4 zeigt, wie die Hashcodes miteinander vereinigt werden.
Der vorangehende Hashcode wird mit 37 multipliziert und der nächste Hashcode wird addiert. Als Startwert (siehe Zeile 02) kann eine beliebige positive Integer-Zahl verwendet werden. Auch der Wert 37 ist nicht zwingend. Als Multiplikator sollten jedoch keine geraden Zahlen verwendet werden.
Wohl gemerkt, Abbildung 4 stellt eine einfache Möglichkeit dar, einen funktionierenden Hashcode zu erzeugen. Auf eine theoretische Betrachtung des Algorithmus wird an dieser Stelle verzichtet.
Die IDE Eclipse bietet eine automatische Implementierung von hashCode an, verwendet aber andere Zahlenwerte.
Mühelos: Objekte ohne Identität
Auch das gibt es: Objekte, die keine Identität haben. Das prominenteste Beispiel dazu ist das Objekt Singleton. Dieses Objekt ist einmalig. In diesem Fall ist die Mühe der Eigenentwicklung von equals und hashCode vergeblich.
Im Allgemeinen kann man allen Klassen, die einen eher funktionalen Charakter haben, wie Factories, Services etc., eine Identität absprechen. Für diese ist die Standardimplementierung der identitätsstiftenden Methoden hinreichend.
Fazit
Objekte, die umgangssprachlich eine eigene Identität haben, wie beispielsweise Kunde oder Artikel müssen mit fachlich sinnvollen Implementierungen der Methoden equals und hashCode ausgestattet sein. Die Verantwortung der Implementierung liegt bei den Entwicklern der zugehörigen Klassen.
Es ist nur natürlich, dass diese Methoden direkt oder indirekt im Leben der Objekte aufgerufen werden. Eine fehlende oder fehlerhafte Implementierung kann zu erheblichen Aufwänden bei der Fehlersuche führen.


