Grundlegender Java-Hashcode und entspricht Demonstrationen

Ich benutze diesen Blog oft, um hart erarbeitete Lektionen in den Grundlagen von Java zu wiederholen. Dieser Blog-Beitrag ist ein solches Beispiel und konzentriert sich auf die Darstellung der gefährlichen Kraft hinter den Methoden equals (Object) und hashCode (). Ich werde nicht jede Nuance dieser beiden hochbedeutenden Methoden behandeln, die alle Java-Objekte explizit deklariert oder implizit von einem übergeordneten Objekt geerbt haben (möglicherweise direkt von Object selbst), aber ich werde einige der häufigsten Probleme behandeln, die auftreten, wenn diese auftreten nicht implementiert oder nicht korrekt implementiert. Ich versuche auch anhand dieser Demonstrationen zu zeigen, warum es für sorgfältige Codeüberprüfungen, gründliche Komponententests und / oder werkzeugbasierte Analysen wichtig ist, die Richtigkeit der Implementierungen dieser Methoden zu überprüfen.

Da letztendlich alle Java-Objekte Implementierungen für equals(Object)und erben hashCode(), melden der Java-Compiler und der Java-Laufzeitstarter beim Aufrufen dieser "Standardimplementierungen" dieser Methoden kein Problem. Wenn diese Methoden benötigt werden, sind die Standardimplementierungen dieser Methoden (wie ihre Cousine die toString-Methode) leider selten erwünscht. Die Javadoc-basierte API - Dokumentation für die Objektklasse diskutiert den „Vertrag“ erwartet von der Anwendung der der equals(Object)und hashCode()Methoden und erörtert auch die wahrscheinlich Standardimplementierung von jedem wenn sie nicht von untergeordneten Klassen außer Kraft gesetzt.

Für die Beispiele in diesem Beitrag verwende ich die HashAndEquals-Klasse, deren Codeliste neben der Verarbeitung von Objektinstanziierungen verschiedener Personenklassen mit unterschiedlichem Unterstützungsgrad hashCodeund unterschiedlichen equalsMethoden angezeigt wird .

HashAndEquals.java

package dustin.examples; import java.util.HashSet; import java.util.Set; import static java.lang.System.out; public class HashAndEquals { private static final String HEADER_SEPARATOR = "======================================================================"; private static final int HEADER_SEPARATOR_LENGTH = HEADER_SEPARATOR.length(); private static final String NEW_LINE = System.getProperty("line.separator"); private final Person person1 = new Person("Flintstone", "Fred"); private final Person person2 = new Person("Rubble", "Barney"); private final Person person3 = new Person("Flintstone", "Fred"); private final Person person4 = new Person("Rubble", "Barney"); public void displayContents() { printHeader("THE CONTENTS OF THE OBJECTS"); out.println("Person 1: " + person1); out.println("Person 2: " + person2); out.println("Person 3: " + person3); out.println("Person 4: " + person4); } public void compareEquality() { printHeader("EQUALITY COMPARISONS"); out.println("Person1.equals(Person2): " + person1.equals(person2)); out.println("Person1.equals(Person3): " + person1.equals(person3)); out.println("Person2.equals(Person4): " + person2.equals(person4)); } public void compareHashCodes() { printHeader("COMPARE HASH CODES"); out.println("Person1.hashCode(): " + person1.hashCode()); out.println("Person2.hashCode(): " + person2.hashCode()); out.println("Person3.hashCode(): " + person3.hashCode()); out.println("Person4.hashCode(): " + person4.hashCode()); } public Set addToHashSet() { printHeader("ADD ELEMENTS TO SET - ARE THEY ADDED OR THE SAME?"); final Set set = new HashSet(); out.println("Set.add(Person1): " + set.add(person1)); out.println("Set.add(Person2): " + set.add(person2)); out.println("Set.add(Person3): " + set.add(person3)); out.println("Set.add(Person4): " + set.add(person4)); return set; } public void removeFromHashSet(final Set sourceSet) { printHeader("REMOVE ELEMENTS FROM SET - CAN THEY BE FOUND TO BE REMOVED?"); out.println("Set.remove(Person1): " + sourceSet.remove(person1)); out.println("Set.remove(Person2): " + sourceSet.remove(person2)); out.println("Set.remove(Person3): " + sourceSet.remove(person3)); out.println("Set.remove(Person4): " + sourceSet.remove(person4)); } public static void printHeader(final String headerText) { out.println(NEW_LINE); out.println(HEADER_SEPARATOR); out.println("= " + headerText); out.println(HEADER_SEPARATOR); } public static void main(final String[] arguments) { final HashAndEquals instance = new HashAndEquals(); instance.displayContents(); instance.compareEquality(); instance.compareHashCodes(); final Set set = instance.addToHashSet(); out.println("Set Before Removals: " + set); //instance.person1.setFirstName("Bam Bam"); instance.removeFromHashSet(set); out.println("Set After Removals: " + set); } } 

Die obige Klasse wird so wie sie ist wiederholt verwendet, mit nur einer geringfügigen Änderung später im Beitrag. Die PersonKlasse wird jedoch geändert, um die Wichtigkeit von equalsund widerzuspiegeln hashCodeund um zu demonstrieren, wie einfach es sein kann, diese durcheinander zu bringen, während es gleichzeitig schwierig ist, das Problem aufzuspüren, wenn ein Fehler vorliegt.

Keine expliziten equalsoder hashCodeMethoden

Die erste Version der PersonKlasse bietet weder eine explizite überschriebene Version der equalsMethode noch der hashCodeMethode. Dies zeigt die "Standardimplementierung" jeder dieser Methoden, von denen geerbt wurde Object. Hier ist der Quellcode für Personohne hashCodeoder equalsexplizit überschrieben.

Person.java (kein expliziter hashCode oder gleichwertige Methode)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Diese erste Version Personbietet keine / set - Methoden erhalten und bietet keine equalsoder hashCodeImplementierungen. Wenn die Hauptdemonstrationsklasse HashAndEqualsmit Instanzen dieser Klasse equalsohne und hashCodeohne ausgeführt wird Person, werden die Ergebnisse wie im nächsten Screenshot gezeigt.

Aus der oben gezeigten Ausgabe können mehrere Beobachtungen gemacht werden. Erstens wird ohne explizite Implementierung einer equals(Object)Methode keine der Instanzen von Personals gleich angesehen, selbst wenn alle Attribute der Instanzen (die beiden Zeichenfolgen) identisch sind. Dies liegt daran, dass die Standardimplementierung, wie in der Dokumentation zu Object.equals (Object) erläutert, equalsauf einer genauen Referenzübereinstimmung basiert:

Die Methode equals für die Klasse Object implementiert die bestmögliche Äquivalenzbeziehung für Objekte. Das heißt, für alle Nicht-Null-Referenzwerte x und y gibt diese Methode genau dann true zurück, wenn x und y auf dasselbe Objekt verweisen (x == y hat den Wert true).

Eine zweite Beobachtung aus diesem ersten Beispiel ist, dass der Hash-Code für jede Instanz des PersonObjekts unterschiedlich ist, selbst wenn zwei Instanzen für alle ihre Attribute dieselben Werte verwenden. Das HashSet wird zurückgegeben, truewenn ein "eindeutiges" Objekt (HashSet.add) zum Satz falsehinzugefügt wird oder wenn das hinzugefügte Objekt nicht als eindeutig betrachtet wird und daher nicht hinzugefügt wird. In ähnlicher Weise wird die HashSetMethode 'remove' zurückgegeben, wenn das angegebene Objekt trueals gefunden und entfernt betrachtet wird oder falsewenn das angegebene Objekt als nicht Teil des Objekts betrachtet wird HashSetund daher nicht entfernt werden kann. Da die equalsund hashCodegeerbten Standardmethoden diese Instanzen als völlig unterschiedlich behandeln, ist es keine Überraschung, dass alle zum Satz hinzugefügt und alle erfolgreich aus dem Satz entfernt werden.

equalsNur explizite Methode

Die zweite Version der PersonKlasse enthält eine explizit überschriebene equalsMethode, wie in der nächsten Codeliste gezeigt.

Person.java (explizit gleich Methode bereitgestellt)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Wenn Instanzen davon Personmit equals(Object)explizit definierten verwendet werden, erfolgt die Ausgabe wie im nächsten Screenshot gezeigt.

Die erste Beobachtung ist, dass jetzt die equalsAufrufe der PersonInstanzen tatsächlich zurückkehren, truewenn das Objekt in Bezug auf alle Attribute gleich ist, anstatt auf eine strikte Referenzgleichheit zu prüfen. Dies zeigt, dass die benutzerdefinierte equalsImplementierung auf Personihre Aufgabe erfüllt hat. Die zweite Beobachtung ist, dass die Implementierung der equalsMethode keinen Einfluss auf die Fähigkeit hatte, das scheinbar gleiche Objekt zum und hinzuzufügen HashSet.

Explizite equalsund hashCodeMethoden

Es ist jetzt an der Zeit hashCode(), der PersonKlasse eine explizite Methode hinzuzufügen . Dies hätte tatsächlich getan werden müssen, als die equalsMethode implementiert wurde. Der Grund hierfür ist in der Dokumentation zur Object.equals(Object)Methode angegeben:

Beachten Sie, dass es im Allgemeinen erforderlich ist, die hashCode-Methode zu überschreiben, wenn diese Methode überschrieben wird, um den allgemeinen Vertrag für die hashCode-Methode aufrechtzuerhalten, der besagt, dass gleiche Objekte gleiche Hash-Codes haben müssen.

Hier ist Personmit einer explizit implementierten hashCodeMethode, die auf den gleichen Attributen Personwie die equalsMethode basiert .

Person.java (explizite Equals- und HashCode-Implementierungen)

package dustin.examples; public class Person { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode() { return lastName.hashCode() + firstName.hashCode(); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Die Ausgabe der Ausführung mit der neuen PersonKlasse mit hashCodeund equalsMethoden wird als Nächstes angezeigt.

Es ist nicht überraschend, dass die für Objekte mit denselben Attributwerten zurückgegebenen Hash-Codes jetzt dieselben sind. Die interessantere Beobachtung ist jedoch, dass wir dem HashSetJetzt nur zwei der vier Instanzen hinzufügen können . Dies liegt daran, dass beim dritten und vierten Additionsversuch versucht wird, ein Objekt hinzuzufügen, das bereits zum Satz hinzugefügt wurde. Da nur zwei hinzugefügt wurden, können nur zwei gefunden und entfernt werden.

Das Problem mit veränderlichen HashCode-Attributen

Für das vierte und letzte Beispiel in diesem Beitrag sehe ich mir an, was passiert, wenn die hashCodeImplementierung auf einem Attribut basiert, das sich ändert. In diesem Beispiel wird eine setFirstNameMethode hinzugefügt Personund der finalModifikator aus seinem firstNameAttribut entfernt. Außerdem muss in der Hauptklasse HashAndEquals der Kommentar aus der Zeile entfernt werden, in der diese neue Set-Methode aufgerufen wird. Die neue Version von Personwird als nächstes angezeigt.

package dustin.examples; public class Person { private final String lastName; private String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } @Override public int hashCode() { return lastName.hashCode() + firstName.hashCode(); } public void setFirstName(final String newFirstName) { this.firstName = newFirstName; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (this == obj) { return true; } if (this.getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Die beim Ausführen dieses Beispiels generierte Ausgabe wird als Nächstes angezeigt.