|
|||||||||||||||||||||||||||||||||||||||||
HOME | COURSES | TALKS | ARTICLES | GENERICS | LAMBDAS | IOSTREAMS | ABOUT | CONTACT | | | | |||||||||||||||||||||||||||||||||||||||||
|
Implementing the hashCode() Method
|
||||||||||||||||||||||||||||||||||||||||
In den letzten beiden Artikeln dieser Kolumne (/
KRE1
/
und /
KRE2
/)haben wir uns mit der Methode equals()
befasst. In dieser Ausgabe wollen wir und ansehen, wie und warum man die
Methode hashCode() implementieren muss. equals() und hashCode() hängen
eng zusammen und müssen konsistent zueinander implementiert werden.
Immer dann, wenn man equals() implementiert hat, muss man auch hashCode()
implementieren. Worin besteht der Zusammenhang? Was genau ist die Konsistenzanforderung?
Wie implementiert man hashCode()?
Hash-basierte Container in JavaDie Methode hashCode() berechnet zu dem Objekt, auf dem sie gerufen wird, einen Hash-Code. Der Hash-Code ist ein integraler Wert, der verwendet wird, um Objekte in einem hash-basierten Container abzulegen oder sie in einem solchen Container zu finden. Die hash-basierten Container in Java sind java.util.Hashtable, java.util.HashMap, java.util.HashSet und deren Subklassen.
Hash-basierte Container gehören zu den Standard-Datenstrukturen
in der Informatik und sind in der entsprechenden Standardliteratur über
Datenstrukturen und Algorithmen beschrieben (siehe zum Beispiel /
KNU
/
oder /
SED
/). Hier ein kurzer Abriss über
die wesentlichen Elemente; siehe auch Abbildung 1, welche den logischen
Aufbau eines hash-basierten Containers zeigt.
Ein hash-basierter Container ist so organisiert, dass er verschiedene Sektionen anlegt (sogenannte "buckets"), in denen die zu speichernden Objekte sequentiell abgelegt werden. Bei den hash-basierten Containern in Java ist zu beachten, dass genau genommen nicht die Objekte, sondern lediglich Referenzen auf die Objekte gespeichert werden. Einen Bucket kann man sich daher als Array oder Liste von Object-Referenzen vorstellen. Der Zugriff auf die verschiedenen Buckets erfolgt über einen integralen Index und ist damit hochperformant; er erfolgt in konstanter Zeit, d.h. der Zugriff auf den Bucket dauert immer gleich lang unabhängig von der Zahl der Elemente im Container. Innerhalb eines Buckets ist der Zugriff auf die Elemente allerdings langsam, weil er nämlich sequentiell erfolgt. Die Abhängigkeit ist hier linear, d.h. es dauert um so länger, je größer der Bucket ist. Deshalb ist ein hash-basierter Container mit vielen kleinen Buckets günstiger als einer mit wenigen großen Buckets. Der integrale Index, der für das Auffinden des Buckets verwendet wird, bestimmt sich über den Hash-Code, den die Methode hashCode() berechnet. Das bedeutet, dass alle Objekte mit demselben Hash-Code im selben Bucket abgelegt sind. Schauen wir uns das noch einmal im Detail an. Betrachten wir als Beispiel das Einfügen eines Objekts in einen HashSet; das geht über die Methode add(Object o). Da wird zunächst der Hash-Code des Objekts o berechnet. Nehmen wir mal an, o.hashCode() liefert den Hash-Code 4711. Damit ist der Bucket identifiziert, in den das Objekt gehört. Dann wird im Bucket Nr. 4711 nachgesehen, ob es das Objekt o dort schon gibt; das ist nötig, weil im HashSet keine Duplikate erlaubt sind. Wie das Vorhandensein eines Objekts im Bucket festgestellt wird, sehen wir uns gleich noch genauer an. Wenn das Objekt im Bucket noch nicht vorhanden ist, dann wird eine Referenz auf das Objekt o im Bucket Nr. 4711 am Ende hinzugefügt. Andernfalls, wird ein Fehler gemeldet, indem der Returnwert false zurück gegeben wird. Das Auffinden des möglichen Duplikats erfolgt wie gesagt sequentiell; es werden alle Objekte im Bucket nacheinander überprüft. Wie oben schon erwähnt, liegen in einem Bucket alle Objekte, deren Hash-Code gleich ist. Das heißt aber nicht, dass deshalb alle Objekte im Bucket gleich sind. Es könnte beispielsweise sein, dass 2 Objekte a und b voneinander verschieden sind, aber die hashCode()-Methode berechnet denselben Hash-Code für die beiden Objekte a und b. Dann landen zwar beide Objekte im gleichen Bucket, sind aber deshalb nicht gleich. Mit Gleichheit ist hier im übrigen die Gleichheit im Sinne von equals() gemeint. In der Tat ruft die Methode add(Object o) auf jedem Element im Bucket die Methode equals() auf. Die übrigen Operationen auf einem hash-basierten funktionieren ganz analog. Es wird immer diese zweistufige Kombination von hashCode() und equals() verwendet, um auf Elemente im Container zuzugreifen. Aus dieser Implementierung der hash-basierten Container in Java ergibt sich eine enge Beziehung zwischen den beiden Methoden equals() und hashCode(). Die beiden Methoden müssen zueinander konsistent sein. Wenn diese Konsistenz nicht gegeben ist, dann passieren seltsame Dinge, die man in erster Näherung mit "Der Hash-Container funktioniert nicht." beschreiben könnte. Details sehen wir uns später noch an. Aus der geschilderten Organisation der hash-basierten Container ergeben sich zwei grundsätzliche Anforderung an den Algorithmus zur Hash-Code-Berechnung.
Der sogenannte hashCode-ContractWas genau ist die Anforderung an eine Implementierung von hashCode(), die konsistent zu equals() ist? Die Anforderungen an hashCode() sind im sogenannten hashCode-Contract beschrieben. Den findet man in der JavaDoc der Java 2 Standard Edition (J2SE) unter dem Eintrag Object.hashCode. Hier ist der Originaltext:
Returns a hash code value for the object. This method is supported for the benefit of hashtables such as those provided by java.util.Hashtable. The general contract of hashCode is:
Probleme mit inkorrekten Implementierungen von hashCode()Bevor wir uns in die Details einer Implementierung von hashCode() vertiefen, widmen wir uns zunächst erst einmal der Frage: was passiert eigentlich, wenn man hashCode() nicht implementiert, oder falsch implementiert?Ebenso wie die equals()-Methode ist auch die hashCode()-Methode bereits in der Superklasse aller Klassen, nämlich Object, definiert. Wenn man hashCode() nicht implementiert, dann erbt die Klasse die Default-Implementierung aus der Superklasse Object. Das hat zur Folge, dass man für alle Java-Objekte einen Hash-Code berechnen kann. Das heißt aber auch, dass man sich für jede Klasse überlegen muss, ob die Implementierung von hashCode() für diese Klasse korrekt ist. Was tut die geerbte Default-Implementierung denn eigentlich? Was Object.hashCode() macht, ist nicht so genau definiert. Die JavaDoc-Beschreibung sagt dazu: As much as is reasonably practical, the hashCode method defined by class Object does return distinct integers for distinct objects. (This is typically implemented by converting the internal address of the object into an integer, but this implementation technique is not required by the JavaTM programming language.)Das heißt, typischerweise basiert die Default-Hash-Code-Berechnung auf der Adresse des Objekts. Das macht Sinn, weil es die Anforderungen des hashCode-Contracts erfüllt, solange auch die Default-Implementerung von equals() nicht überschrieben wird. Zur Erinnerung: die Default-Implementierung von equals() in der Klasse Object vergleicht die Adressen der beiden Objekte. Die oben spezifizierte Implementierung von hashCode() ist dazu konsistent: es geht in beide Methoden lediglich die Adresse des Objekts ein. Damit ist gewährleistet, dass gleiche Objekte (d.h. solche mit gleicher Adresse) gleiche Hash-Codes haben (nämlich solche, die sich aus der Adresse berechnen lassen). Sobald man sich entschließt, für eine Klasse die equals() -Methode zu überschreiben, dann muss man auch die hashCode()-Methode überschreiben, weil sonst mit größter Wahrscheinlichkeit der hashCode-Contract verletzt ist. Welche Auswirkungen haben solche Verletzungen des hashCode-Contracts? Betrachten wir das Beispiel einer Klasse PhoneNumber, die zwar equals() überschreibt, aber hashCode() nicht. Die equals()-Methode wird dann den Inhalt der PhoneNumber-Objekte vergleichen, also beispielsweise Vorwahl und Anschlussnummer. Die geerbte hashCode()-Methode berechnet aber den Hash-Code aus der Adresse des Objekts. Dann passiert das Folgende, wenn man beispielsweise die Telefonnummer als Schlüssel in einer HashMap verwenden will: man kann zwar Key-Value-Paare in die HashMap eintragen, also beispielsweise den Namen zu einer bestimmten Telefonnummer, aber man wird sie u.U. nie mehr wiederfinden. Map createPhoneDirectory()In diesem Beispiel wird als Schlüssel für das Eintragen und das Finden die gleiche Telefonnummer benutzt, nämlich (501) 437 5493. Der betreffende Eintrag zu dieser Nummer wird aber nicht gefunden, obwohl er in der HashMap vorhanden ist. Das liegt daran, dass "gleiche" PhoneNumber-Objekte nicht die gleichen Hash-Codes haben. In obigem Beispiel haben wir zwei PhoneNumber-Objekte mit gleichem Inhalt, aber mit unterschiedlichen Adressen verwendet. Die Methode HashMap.put() legt daher den Eintrag in einem Bucket ab, dessen Identifikation sich aus der Adresse des ersten PhoneNumber-Objekts ergibt, und die Methode hashMap.get() sucht den Eintrag in einem ganz anderen Bucket, dessen Identifikation sich aus der Adresse des zweiten PhoneNumber-Objekts ergibt. Dort ist der Eintrag aber nicht zu finden. Es wäre Zufall, wenn unter diesen Umständen überhaupt noch Einträge gefunden würden. Man sieht an diesem Beispiel, dass die Verletzung der Forderung "Gleiche Objekte müssen gleiche Hash-Codes haben" dazu führt, dass die hash-basierten Container nicht funktionieren. Die Konsistenz zwischen hashCode() und equals() ist von fundamentaler Bedeutung für das Arbeiten mit hash-basierten Containern in Java. Wichtig für das Funktionieren der hash-basierten Container sind dabei die ersten beiden Anforderungen ("gleicher Hash-Code bei wiederholten Aufrufen" und "gleiche Hash-Codes für gleiche Objekte"). Die dritte Anforderung ist eigentlich keine Forderung, sondern lediglich ein Hinweis, dass die Performance besser ist, wenn die berechneten Hash-Codes für verschiedene Objekte nach Möglichkeit verschieden sind. Hash-Codes und PersistenzDie erste Anforderung des hashCode-Contracts enthält den Zusatz, dass die Hash-Codes bei verschiedenen Programmläufen durchaus verschieden sein dürfen. Diese Aussage erklärt sich durch die Tatsache, dass die Default-Implementierung von hashCode() in Object typischerweise auf den Adressen der Objekte basiert. Die Adressen können natürlich bei jedem Programmlauf anders sein. Nebeneffekt dieser Tatsache ist, dass zumindest die Default-Implementierung von hashCode() nicht für Persistenz taugt.Man könnte ja auf die Idee kommen, eine HashMap wie unser Telefonbuch aus dem obigen Beispiel persistent zu machen, indem man die Einträge in eine Datenbank schreibt oder sonstwie serialisiert und speichert. Dabei würden die Hash-Codes aus einem Programmlauf für die persistente Speicherung verwendet und beim nächsten Programmlauf wieder eingelesen. Die eingelesenen Hash-Codes sind aber unbrauchbar, weil für das gleiche Objekt diesmal ein ganz anderer Hash-Code berechnet wird.
Wenn man Hash-Codes persistent machen will, dann muss man die hashCode()-Methode
entsprechend implementieren, nämlich so, dass tatsächlich immer
für gleiche Objekte der gleiche Hash-Code berechnet wird. Die
Default-Implementierung leistet dies nicht, und es ist auch von anderen
Implementierungen der hashCode()-Methode nicht verlangt. Das heißt
insbesondere, dass man das auch nicht von der hashCode()-Implementierung
anderer Klassen erwarten darf.
Hash-Codes und Objekt-ReferenzenSelbst wenn man alle oben geschilderten Probleme vermieden hat und hashCode() korrekt implementiert hat, gibt es immer noch Überraschungen und Fehlerquellen, die mit der Referenz-Semantik in Java zu tun haben. In Java enthalten alle Container des JDK, inklusiver der hash-basierten Container, grundsätzlich Referenzen auf Objekte und niemals Kopien der "enthaltenen" Objekte. Infolgedessen erfolgt der Zugriff auf Elemente, die in einem hash-basierten Container abgelegt sind, immer über Referenzen. Wenn diese Referenzen verwendet werden, um das referenzierte Objekt zu modifizieren, dann ist es wahrscheinlich, dass der hash-basierte Container zerstört wird. Hier ist ein Beispiel:AbstractSet mySet = new HashSet();In einem HashSet sind Point-Objekte abgelegt, oder genauer gesagt, Referenzen auf Point-Objekte. Die hasNext()-Methode des Iterators liefert sukzessive Referenzen auf alle enthaltenen Objekte. Diese Referenzen werden in dem Beispielcode verwendet, um die modifizierende Methode setLocation() der Klasse Point zu rufen, die den Inhalt des Point-Objekts verändert. Mit dem Inhalt des Objekts ändert sich aber auch der Hash-Code des Objekts und eigentlich müsste das veränderte Point-Objekt in einem anderen Bucket abgelegt sein. Das ist aber nicht der Fall; das betreffende Point-Objekt bleibt, wo es ist, und befindet sich nach der Modifikation im falschen Bucket. Damit ist der ganze Container zerstört. Das kann sich darin äußern, dass das veränderte Point-Objekt weder im Container gefunden noch aus dem Container entfernt werden kann. Es können sich aber auch andere unerwünschte Effekte ergeben; das Verhalten von Operationen auf einem zerstörten Container ist generell undefiniert.
Die Ursache des Problems liegt in der Referenz-Semantik von Java. Der
Benutzer bekommt über die Referenzen Schreib-Zugriff auf die Objekte
im Container und kann die dort abgelegten Objekte an Ort und Stelle verändern.
Jede Veränderung der Objekte, die das Ergebnis von hashCode()beeinflusst,
zerstört aber den Container, weil das Objekt nach der Veränderung
eigentlich in einem anderen Bucket abgelegt sein müsste. Das
ist ein generelles Problem bei allen Containern, deren Organisation in
irgendeiner Form auf dem Inhalt der gespeicherten Objekten beruht und bei
denen Schreib-Zugriff auf die gespeicherten Objekte möglich ist. In
diese Kategorie fallen alle hash-basierten, aber auch alle baum-basierten
Container in Java. Schützen kann man sich vor dieser Falle nur
durch Programmierdisziplin, indem man es unterlässt, enthaltene Objekte
an Ort und Stelle im Container zu ändern. Man muss statt dessen
das alte Objekte aus dem Container entfernen und das neue "modifizierte"
Objekt in den Container einfügen.
Implementierung von hashCode()Worauf muss man nun achten, wenn man hashCode() implementiert? Wichtig sind die beiden folgenden Aspekte:
Konsistenz zwischen hashCode() und equals()Wie stellt man sicher, dass hashCode() und equals() konsistent zueinander sind? Man muss dafür sorgen, dass in die Berechnung des Hash-Codes nur die Informationen eingehen, die auch für die Implementierung von equals() berücksichtigt werden.In obigem Beispiel der Klasse PhoneNumber mit überschriebenem equals() und geerbtem hashCode() ist dies verletzt. Die geerbte Default-.Implementierung von hashCode() basiert auf der Adresse des Objekts. Die Adresse spielt aber für die Implementierung des überschriebenen equals() keine Rolle, da ein korrektes equals() die Felder des Objekts vergleicht und sich für die Adresse des Objekts überhaupt nicht interessiert. Unter diesen Umständen ist es nicht gewährleistet, dass gleiche Objekte gleiche Hash-Codes haben. Normalerweise wird man also all die Felder in die Hash-Code-Berechnung einbeziehen, die in equals() miteinander verglichen werden und man wird in hashCode() alles ignorieren, was in equals() nicht vorkommt. Das heißt zum Beispiel, dass man die Adresse nicht zur Hash-Code-Berechnung heranziehen darf, wenn die Adresse nicht zur Gleichheit beiträgt. Es heißt aber auch, dass man transiente Felder nicht für die Hash-Code-Berechnung berücksichtigen darf. Transiente Felder tragen zum logischen Inhalt eines Objekts nichts bei und werden deshalb in Implementierungen von equals() ignoriert (siehe /KRE/). Aus diesem Grunde müssen sie auch für die Implementierung von hashCode() ignoriert werden. Das bedeutet aber nicht, dass alle Informationen, die in equals() berücksichtigt werden, auch in hashCode() berücksichtigt werden müssen. Man kann einen Teil der Information, also beispielsweise einen Teil der Felder, für die Hash-Code-Berechnung ignorieren. Das führt zwar dazu, dass ungleiche Objekte gleiche Hash-Codes haben, aber das ist nach der dritten Regel im hashCode-Contract ausdrücklich erlaubt, und manchmal ist es auch durchaus sinnvoll in Hinblick auf die Performanz. Performanz von hashCode()Der Zugriff auf Elemente im einem hash-basierten Container geschieht über eine rasche Identifikation des Bucket mittels Index (= Hash-Code) gefolgt von der relativ langsamen sequentiellen Suche innerhalb des hoffentlich kleinen Bucket. Der Vorteil der hash-basierten Container ist daher der schnelle Zugriff auf den Bucket mittels Index (= Hash-Code). Wenn nun die Berechnung des Hash-Codes ausgesprochen lange dauert, dann ist der Performance-Gewinn durch den schnellen Zugriff per Hash-Code im Handumdrehen zunichte gemacht. Deshalb sollten HashCode-Berechnungen möglichst effizient sein.Die einfachste und schnellste Lösung wäre es, gar keine Berechnungen anzustellen und für alle Objekte immer denselben Integer-Wert zurück zu liefern. Das ist durchaus erlaubt und führt dazu, dass alle Objekte im selben Bucket landen. Dieser Bucket wird riesengroß sein und die sequentielle Suche darin wird reichlich lange dauern. Unter diesen Randbedingungen macht der hash-basierte Container keinen Sinn mehr; da kann man gleich eine verkettete Liste verwenden. Die Hash-Code-Berechnung soll also nicht nur schnell sein, sondern auch zu einem Container mit vielen kleinen Buckets führen. Ziel ist eine möglichst performante Implementierung, die eine möglichst gleichmäßige Verteilung der berechneten Hash-Codes im Interval der möglichen Integer-Werte von -2147483648 bis 2147483647 erreicht. Nun haben wir oben gesagt, dass man wegen der Konsistenz mit equals() alle Felder in die Hash-Code-Berechnung einbeziehen soll, die in equals() miteinander verglichen werden. Das ist auch durchaus praktikabel, solange das Objekt nicht allzu viele Felder hat. Wenn eine Klasse z. B. aber ein größeres Array von Objekten enthält, dann werden zwar alle Array-Elemente zur Bestimmung der Gleichheit mittels equals() beitragen, aber für eine Implementierung von hashCode() wäre die Berücksichtigung sämtlicher Array-Elemente zu aufwendig. Daher würde man bei einem großen Array vielleicht nur jedes n-te Element in der Hash-Code-Berechnung berücksichtigen. Analog kann man auch Felder weglassen, die sowieso bei den meisten Objekten den gleichen Werten haben werden. Solche Felder tragen nichts Relevantes zur Produktion unterschiedlicher Hash-Codes bei, erhöhen aber den Aufwand für die Hash-Code-Berechnung, wenn sie berücksichtigt werden. Man muss auch nicht unbedingt in Klassenhierarchien auf jedem Level jeweils eine neue Version von hashCode() implementieren, um die Subklassen-.spezifischen Felder zu berücksichtigen. Wenn die Superklasse eine korrekte Implementierung von hashCode() zur Verfügung stellt, dann kann man es u.U. dabei belassen. Bevor man also zur eigentlichen Berechnung des Hash-Codes ansetzt, muss man zunächst (für die Konsistenz mit equals()) die Felder identifizieren, die potentiell in die Berechnung eingehen können; das sind genau die Felder, die in equals() miteinander verglichen werden. Aus diesen Feldern wählt man dann diejenigen aus, die man berücksichtigen oder eben ignorieren will, damit eine Lösung entsteht, die einerseits eine vernünftige Verteilung der Hash-Codes erreicht, aber anderseits auch hinreichend effizient ist. Anleitung zur Implementierung von hashCode()Wenn man alle Felder identifiziert hat, die zur Berechnung des Hash-Codes beitragen sollen, wie stellt man dann die Hash-Code-Berechnung an? Die nachfolgend vorgeschlagene Lösung ist keine optimale Implementierung. Zur Berechnung von Hash-Codes gibt es reichlich Information in Fachbüchern. Was wir hier vorstellen wollen, ist ein Rezept für den Hausgebrauch. Wenn man sich daran hält, erzielt man voraussichtlich ein brauchbares, aber nicht notwendig optimales Ergebnis.Die Idee besteht darin, dass man jedem Feld, dass berücksichtigt werden soll, einen Integer-Wert zuordnet und dann all diese Integer-Werte aufaddiert, wobei man noch einen geeigneten Multiplikator verwendet. Man berechnet also nach folgender Formel:
Anfangswert und MultiplikatorMan beginnt mit einem Anfangswert für den Hash-Code (in der Formel: hashcode0). Der Anfangswert ist typischerweise von 0 verschieden ist. In unserem Beispiel (siehe unten) wählen wir 17 als Anfangswert, das ist aber rein willkürlich. Dazu wählt man sich einen Multiplikator. Der Multiplikator ist typischerweise eine nicht zu große Primzahl, in unserem Beispiel 59. Dann geht die Implementierung von hashCode() wie folgt los:public int hashCode() { FeldwertJedem für die Berechnung relevanten Feld des Objekts muss ein Integer-Wert zugeordnet werden. Der zugeordnete Wert hängt vom Typ des Objekts ab. Tabelle 1 zeigt eine Übersicht.
Hier ein paar Erläuterungen zur Umrechnung in Integer-Werte.§ Felder vom Typ Boolean werden einfach auf 0 oder 1 umgewertet.
SonderfälleBei der Implementierung von equals() haben wir Klassen mit inkorrekter Implementierung von equals() gesondert behandeln müssen. Unser Beispiel war die Klasse StringBuffer, die die equals()-Methode nicht überschreibt. Immer wenn Felder vom Typ StringBuffer miteinander verglichen werden müssen, haben wir die StringBuffer in String-Objekte umgewandelt und dann die String-Objekte per String.equals() miteinander verglichen.Für die Implementierung von hashCode() müssen wir analog vorgehen, damit die Konsistenz zwischen hashCode() und equals() gewährleistet ist. Einem StringBuffer-Feld wird daher nicht field.hashCode() zugeordnet, sondern field.toString().hashCode(). Derartige Sonderbehandlungen für all diejenige Felder notwendig, wo sie auch in equals() nötig waren. ArraysArrays werden gesondert behandelt. Bei größeren Arrays wird man wahrscheinlich nur eine Auswahl der Array-Elemente berücksichtigen wollen. Die einzelnen Array-Elemente behandelt man dann ihrem jeweiligen Type entsprechend wie oben beschrieben. Hier ist ein Beispiel, in dem nur die Array-Elemente berücksichtigt werden, deren Index eine Zweierpotenz ist:class MyClass { SuperklassenanteileDie Berücksichtigung von geerbten Feldern delegiert man an die Superklasse. Das heißt, man ruft super.hashCode().public int hashCode() {Das darf man allerdings nur tun, wenn die Superklasse nicht Object ist, weil ja sonst die Adresse des Objekts berücksichtigt würde, und das führt dann zu den schon geschilderten Problemen. So ähnlich wie bei equals(), wird man zwischen direkten und indirekten Subklassen von Object unterscheiden. In den indirekten Subklassen wird man super.hashCode()rufen, während man es in den direkten Subklassen nicht tun wird. hashCode() in KlassenhierarchienIm letzten Artikel haben wir ausführlich diskutiert, ob man in Klassenhierarchien den Vergleich zwischen Super- und Subklassen-Objekten zulassen sollte. Das Ergebnis war im wesentlichen die Erkenntnis, dass man ihn i.a. nicht zulassen wird; dann ist man auf der sicheren Seite. Und wenn man den ihn doch zulassen will, dann sollte er als final-Methode in der Superklasse definiert sein.Die Entscheidung, die man diesbezüglich für equals() getroffen hat, hat Auswirkungen auf die Implementierung von hashCode(). Wenn die equals()-Methode final ist, dann sollte auch die hashCode()-Methode final sein. Denn sonst könnte hashCode()überschrieben werden. Wenn die überschreibende Version von hashCode() Subklassen-spezifische Felder berücksichtigt, dann ist die Konsistenz zu equals() verletzt: zwei Subklassenobjekte könnten dann gleich sein, weil nur Ihre Superklassenanteile miteinander verglichen werden, aber sie hätten verschiedene Hash-Codes, da Subklassen-spezifische Information in die Hash-Code-Berechnung eingeht
Ähnliche Gefahren lauern, wenn der Super-Subklassen-Vergleich erlaubt
ist, ohne dass equals() als final deklariert ist. (Davon haben wir abgeraten,
aber man findet es in der Praxis.) Dann sollte hashCode() nur in der Superklasse
(und dort am besten als final) definiert sein und in der Subklasse auf
keinen Fall überschrieben werden. Wenn man hashCode() in der Subklasse
überschreibt, dann ist wieder die Konsistenz zwischen equals() und
hashCode()verloren: zwei Objekte, ein Super- und ein Subklassen-Objekt,
könnten dann gleich sein, weil nur ihr Superklassenanteil verglichen
wird. Wenn hashCode()aber in Super- und Subklasse in verschiedenen Versionen
existiert, dann werden die Hash-Codes dieser beiden gleichen Objekte nicht
unbedingt gleich sein.
ZusammenfassungWir haben uns in dieser Ausgabe mit der Methode hashCode() befasst, die man auf allen Objekte in Java aufrufen kann. Wir haben gesehen, wann man die Default-Implementierung von hashCode() überschreiben muss, nämlich immer dann wenn man dasselbe für equals() tut. Wir haben den sogenannten hashCode-Contract angesehen, der die Anforderungen an eine Implementierung von hashCode() spezifiziert. Wir haben die Konsequenzen bei Verletzung des hashCode-Contracts gesehen; die hash-basierten Container funktionieren dann nicht. Und schließlich haben wir die Prinzipien einer Implementierung betrachtet; wichtig für eine korrekte Implementierung ist die Konsistenz zu equals() und die Performanz der Hash-Code-Berechnung sowie die Güte der Hash-Code-Verteilung.
Wegen der engen Beziehung zwischen equals() und hashCode() an dieser
Stelle nochmals die Empfehlung, equals() sorgfältig und wohl überlegt
zu implementieren. equals() hat Auswirkungen auf andere Versionen von equals()
in derselben Klassenhierarchie und auf die Hash-Code-Berechnung.
Und das ist noch nicht alles; in der nächsten Ausgabe dieser Kolumne
werden wir unsere Betrachtungen über die Objekt-Infrastruktur fortsetzen
und uns die compareTo()-Methode ansehen, die für die Benutzung der
baum-basierten Container von Bedeutung ist. Auch für compareTo() gibt
es eine Konsistenzanforderung in Bezug auf equals(). Dazu mehr beim nächsten
Mal.
Literaturverweise
|
|||||||||||||||||||||||||||||||||||||||||
© Copyright 1995-2008 by Angelika Langer. All Rights Reserved. URL: < http://www.AngelikaLanger.com/Articles/EffectiveJava/03.HashCode/03.HashCode.html> last update: 26 Nov 2008 |