04.03.11 Besondere Methoden (equals, hashCode und toString)
hashCode
Die Methode hashCode
liefert einen möglichst eindeutigen (aber nicht zwingend einmaligen) Wert in Form eines ints
zur Identifikation des Inhalts eines Objekts zurück – ist also der Methode equals
ähnlich – erwartet aber keinen Übergabeparameter, da dieser Wert anhand der Eigenschaften des implementierenden Objekts berechnet wird. Wenn zwei Objekte einen unterschiedlichen hashCode
besitzen, können Sie nicht inhaltlich gleich sein. Besitzen zwei Objekte einen identischen hashcode
, können sie inhaltlich gleich sein, müssen aber nicht. Außerdem sollte die Berechnung des hashCodes
schnell möglich sein.
Deshalb wird equals
beim Vergleich von mehreren Objekten miteinander oftmals mit hashCode
kombiniert. Zuerst wird „auf die Schnelle“ überprüft, ob beide Objekte den selben hashCode
besitzen. Trifft dies zu, wird die langsamerere equals
-Methode bemüht, um die wirkliche Gleichheit zu testen. Ist der hashCode
allerdings nicht identisch, ist es völlig ausgeschlossen, dass die Objekte dennoch den selben Inhalt besitzen – es kann auf die umfangreiche Überprüfung mit equals
verzichtet werden. Klassen im JDK, die dieses Verfahren einsetzen, sind z. B. java.util.HashSet, java.util.Hashtable, java.util.HashMap und deren Kindklassen. Im Kapitel über das Collection Framework lernen Sie mehr über diese Klassen.
Aus diesem Grund ist es umso wichtiger, dass Sie eine korrekte und schnelle Implementierung von hashCode
wählen!
Beachten Sie, dass Sie sich – ähnlich wie bei
equals
– nicht auf die Standard-Implementierung verlassen können (welche im Falle vonhashCode
nativ realisiert wurde).
Aber wie schaut sie nun aus, die optimale Implementierung von hashCode
? Ein Rezept dafür gibt es leider nicht, aber es gibt zumindest einige Richtlinien, an denen Sie sich halten können/müssen:
- Die Berechnung sollte schnell sein
- Alles, was von
equals
als identisch angesehen wird, hat auch den gleichenhashCode
- Alles, was den selben
hashCode
hat, muss nicht lautequals
identisch sein - In die Berechnung des
hashCodes
dürfen nur Attribute einfliesen, die auch beim Vergleich mitequals
berücksichtigt wurden - Es müssen nicht alle Attribute, die von
equals
berücksichtigt wurden, auch imhashCode
Verwendung finden. Attribute, die meistens den selben Wert haben (oder jedes einzelne Element eines großen Arrays) müssen z. B. oftmals nicht berücksichtigt werden - Jedes Attribut, das in die Berechnung mit einfliest, muss ein
Integer
-Wert zugewiesen werden können - Die
Integer
-Werte der Attribute werden addiert und optional mit einer beliebigen (Prim-)Zahl (nicht zu groß) multipliziert - Der mehrmalige Aufruf der
hashCode
-Methode muss immer den selben Wert zurückliefern – außer der Inhalt des Objekts hat sich seit dem letzten Aufruf verändert, oder der aktuelle Programmlauf wurde abgebrochen
Sie finden in der Auflistung oben den Hinweis, dass jedes Attribut einen Integer
-Wert zugewiesen bekommen muss. Für primitive Datentypen wie byte, short, char
und int
sollte der Integer
-Wert klar sein. Aber was passiert mit long, double, boolean
, Objekten, …!? Hierzu existieren auch einige Richtlinien:
Typ | Integer-Wert |
byte, short, char, int |
Wird in int gecastet => (int)var |
boolean |
Für true = 1, für false = 0 => (var ? 1 : 0) |
long |
Wird in zwei Integer umgerechnet => (int)(var & 0xFFFFFFFF) und (int)(var >>> 32) |
float |
Wird in sein Bit-Layout gewandelt, außer wenn der Wert = 0.0 oder -0.0 ist => ((var==0.0F) ? 0 : Float.floatToIntBits(var)) |
double |
Wird in sein Bit-Layout gewandelt, außer wenn der Wert = 0.0 oder -0.0 ist => ((var==0.0) ? 0L : Double.doubleToLongBits(var)) daraus entsteht ein long der noch mal (wie oben) behandelt werden muss |
Objekte | Aufruf der hashCode -Methode, sofern hashCode von dem Objekt implementiert wurde und das Objekt ungleich null ist => ((var==null) ? 0 : var.hashCode()) |
Der eigentliche Algorithmus sieht dann in etwa so aus:
public int hashCode() { int hashCode = 11; // willkürlicher Initialwert int multi = 29; // nicht zu große, zufällig gewählte Primzahl als Multiplikator hashCode += this.anInt; hashCode = hashCode * multi + (int)this.aChar; hashCode = hashCode * multi + (this.anObject == null ? 0 : this.anObject.hashCode()); hashCode = hashCode * multi + (int)(this.aLong & 0xFFFFFFFF); hashCode = hashCode * multi + (int)(this.aLong >>> 32); ... return hashCode; }
Sollte Ihre Klasse von einer anderen Klasse erben, die ebenfalls eine eigene Implementierung von hashCode
besitzt, ist es meistens sinnvoll den hashCode
der Elternklasse mit einzubeziehen (super.hashCode();
).
Vertrag/contract für hashCode und equals
Wenn Sie eine eigene equals
– und/oder hashCode
-Methode in Ihrer Klasse ausprogrammieren, gibt es Tipps, wie Sie das am Besten angehen könnten (dieses Kapitel), aber auch Implementierungsregeln (Verträge/contracts), die Sie einhalten sollten, wozu Sie aber niemand zwingen kann. Diese Regeln finden Sie in der JavaDoc bei den entsprechenden Methoden der Klasse Object
=> equals und hashCode. Ins Deutsche übersetzt bedeuten sie sinngemäß:
- equals
- Wird ein Objekt mit sich selbst verglichen, wird
true
zurückgeliefert - Der Vergleich von Objekt 1 mit Objekt 2 muss das selbe Ergebnis liefern, wie der Vergleich von Objekt 2 mit Objekt 1
- Wenn Objekt 1 gleich Objekt 2 ist und Objekt 2 gleich Objekt 3, dann muss Objekt 1 auch gleich Objekt 3 sein
- Auch nach n Vergleichen zweier Objekte verändert sich das Resultat nicht
- Kein Objekt entspricht
null
- hashCode
- Egal wann
hashCode
aufgerufen wurde – der Rückgabewert ist immer gleich. Dies gilt aber nur, wenn
1.) Sich der Inhalt des Objekts seit dem letzten Aufruf nicht verändert hat
2.) Das Programm zwischenzeitlich nicht beendet wurde - Sind zwei Objekte gleich, generieren sie auch den selben
hashCode
- Sind zwei Objekte nicht gleich, müssen sie keinen unterschiedlichen
hashCode
haben – es wäre aber besser, wenn möglichst wenige bis gar keine Objekte den selbenhashCode
generieren
toString
Nachdem die letzten beiden Methoden etwas mit dem Vergleich von Objekten zu tun hatten, beschäftigt sich diese Methode mit etwas anderem – der Repräsentation eines Objekts als String
.
Manchmal kommt es vor, dass Sie dem Benutzer (oder zur Überprüfung sich selbst) ein Objekt einer Klasse durch einen individuellen Text zeigen möchten – das menschliche Augen kann eben recht wenig mit Bytes, Hash-Codes oder einer equals
-Methode anfangen. Hierzu fassen Sie die wichtigsten Informationen in der toString
Methode Ihrer Klasse zusammen und geben sie in Form eines Strings
zurück. Auch hier ist es wieder wichtig, dass Sie genau die toString
-Methode verwenden, wie sie durch die Klasse Object
vorgegeben wurde (
public String toString()
), da toString
von anderen Klassen als Standardpräsentation des Objekts gesehen wird. Wenn Sie z. B. ein Objekt auf der Konsole ausgeben wollen, wird der Rückgabewert der toString
-Methode ausgegeben.
Object obj = new Object(); String str = new String("Java-Blog-Buch.de ist toll!"); System.out.println(str); System.out.println(str.toString()); System.out.println(obj); System.out.println(obj.toString());
Natürlich müssen Sie die toString
-Methode nicht überschreiben. Eine Klasse, die nie für einen Menschen lesbar dargestellt werden muss (wie z. B. eine Klasse zur Verschlüsselung), braucht auch keine toString
-Methode. Sobald es aber relevant wird, eine Klasse für Menschen lesbar zu machen (z. B. für eine Klasse, die eine Person repräsentiert), sollten Sie toString
überschreiben. Überschreiben Sie die toString
-Methode nicht, rufen aber dennoch selbige auf, erhalten Sie eine etwas kryptische Ausgabe wie im Beispiel oben, als wir das Objekt obj
ausgegeben haben => java.lang.Object@7f5a7f5a
. Doch wie kommt diese Ausgabe zustande? Um das zu analysieren, sehen wir uns die Standardimplementierung der toString
-Methode an:
public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); }
Es wird also der Name der Klasse, gefolgt von einem „@“ und dem Hash-Code des Objekts in hexadezimaler Schreibweise ausgegeben. Wie Sie toString
sinnvoll überschreiben, hängt natürlich von der Klasse ab. Deshalb folgt jetzt ein kleines Beispiel in Form einer Klasse Person
.
Zuerst programmieren wir die eigentliche Klasse. Sie erhält die Attribute firstname
, lastname
und age
mitsamt den benötigten Getter- und Settermethoden.
public class Person { private String firstname = null; private String lastname = null; private int age = 0; public int getAge() { return this.age; } public void setAge(int age) { this.age = age; } public String getFirstname() { return this.firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getLastname() { return this.lastname; } public void setLastname(String lastname) { this.lastname = lastname; } }
Als nächsten Schritt erstellen Sie sich eine weitere Klasse mit einer Main-Methode. Dort erzeugen Sie ein Array mit drei Personen, die Sie dann in einer Schleife ausgeben:
public static void main(String[] args) { Person[] persons = new Person[3]; persons[0] = new Person(); persons[1] = new Person(); persons[2] = new Person(); persons[0].setAge(21); persons[0].setFirstname("Andreas"); persons[0].setLastname("Pries"); persons[1].setAge(26); persons[1].setFirstname("Sebastian"); persons[1].setLastname("Würkner"); persons[2].setAge(20); persons[2].setFirstname("Stefan"); persons[2].setLastname("Kiesel"); for (int i = 0; i < persons.length; i++) { System.out.println(persons[i]); } }
Natürlich erscheint auch diesmal die nichts sagende Ausgabe des Klassennamen in Kombination mit dem Hash-Code. Deshalb legen wir nun noch eine entsprechende toString
-Methode in unserer Klasse an:
public String toString() { return this.lastname + ", " + this.firstname; }
und testen das Programm erneut. Nun erscheint eine schönere Ausgabe auf der Konsole.
Pries, Andreas
Würkner, Sebastian
Kiesel, Stefan
Auf der nächsten Seite werden wir unser Beispiel zu toString
noch etwas erweitern.
Besserer Stil wäre:
Hallo m3,
danke für Ihren Kommentar! Da das Java Blog Buch ein Buch ist, sollten die Kapitel mehr oder weniger aufeinander aufbauen. Leider wird
instanceof
erst später im Kapitel 04.05 Vererbung angesprochen, weshalb ich mich gegen die Verwendung voninstanceof
entschieden habe. Aber selbstverständlich funktioniert es auch mitinstanceof
, da haben Sie natürlich Recht.Eine Anmerkung zu Ihrem Code/Kommentar im Code habe ich aber trotzdem:
>> den Kram darüber braucht man nicht – null wird durch instanceof geregelt
Dennoch sollte überprüft werden, ob es sich beim übergebenen Objekt um das selbe Objekt wie
this
handelt, da dies vom contract so verlangt wird.Gruß
Stefan
Hallo Stefan,
Ich verstehe das ein Prüfung, ob es sich um das selbe Objekt handelt, sinnvoll sein kann – gerade wenn der Vergleichsalgorithmus zeitaufwendig ist (hier nicht der Fall).
Ich verstehe jedoch nicht, was du mit „contract“ meinst.
Gruß
m3
Hallo m3,
genau das ist der Grund, warum man überprüfen sollte, ob es sich um das identische Objekt handelt. Der „contract“ ist der Vertrag bzw. die Vorgaben von Sun, wie
equals
implementiert werden sollte. Dieser wird auf der zweiten Seite dieses Kapitels unter dem Punkt Vertrag/contract für hashCode und equals besprochen.Gruß
Stefan
Ah ok, danke
Gruß
m3
Sollten Attribute, die vom Typ float oder double sind nicht besser mit Float.compare bzw. Double.compare verglichen werden und abhängig vom Ergebnis entschieden werden, ob die betrachteten Attribute als gleich oder unterschiedlich gelten?
Hallo Thomas,
wirft man einen Blick in den Quellcode von
Float.compare
stellt man fest, dass hier nicht diefloat
-Werte sondern die Bit-Repräsentationen in Form vonIntegern
verglichen werden. Sie haben also recht, dassFloat.compare
(und analog dazu auch Double.compare) etwas anderes als ein Vergleich mit==
ist. In der Praxis fällt dies jedoch kaum ins Gewicht, weshalb ich den Artikel gerne unverändert, und Ihren Kommentar als zusätzlichen Hinweis da stehen lassen würde.Gruß
Stefan
Der Gebrauch von instanceof in equals()-Methoden (und dadurch die Miterfassung von Subklassen im Vergleich) ist schlecht, weil auch dadurch der Kontrakt der equals()-Methode verletzt wird. Siehe dazu „Effective Java, Second Edition“, Rezept 8.
Der im Artikel gewählte Ansatz ist der Empfohlene.
Hallo Tobias,
vielen Dank für die zusätzliche Information.
Gruß
Stefan