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 hashCode
und unterschiedlichen equals
Methoden 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 Person
Klasse wird jedoch geändert, um die Wichtigkeit von equals
und widerzuspiegeln hashCode
und 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 equals
oder hashCode
Methoden
Die erste Version der Person
Klasse bietet weder eine explizite überschriebene Version der equals
Methode noch der hashCode
Methode. Dies zeigt die "Standardimplementierung" jeder dieser Methoden, von denen geerbt wurde Object
. Hier ist der Quellcode für Person
ohne hashCode
oder equals
explizit ü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 Person
bietet keine / set - Methoden erhalten und bietet keine equals
oder hashCode
Implementierungen. Wenn die Hauptdemonstrationsklasse HashAndEquals
mit Instanzen dieser Klasse equals
ohne und hashCode
ohne 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 Person
als 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, equals
auf einer genauen Referenzübereinstimmung basiert:
Eine zweite Beobachtung aus diesem ersten Beispiel ist, dass der Hash-Code für jede Instanz des Person
Objekts unterschiedlich ist, selbst wenn zwei Instanzen für alle ihre Attribute dieselben Werte verwenden. Das HashSet wird zurückgegeben, true
wenn ein "eindeutiges" Objekt (HashSet.add) zum Satz false
hinzugefügt wird oder wenn das hinzugefügte Objekt nicht als eindeutig betrachtet wird und daher nicht hinzugefügt wird. In ähnlicher Weise wird die HashSet
Methode 'remove' zurückgegeben, wenn das angegebene Objekt true
als gefunden und entfernt betrachtet wird oder false
wenn das angegebene Objekt als nicht Teil des Objekts betrachtet wird HashSet
und daher nicht entfernt werden kann. Da die equals
und hashCode
geerbten 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.
equals
Nur explizite Methode
Die zweite Version der Person
Klasse enthält eine explizit überschriebene equals
Methode, 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 Person
mit equals(Object)
explizit definierten verwendet werden, erfolgt die Ausgabe wie im nächsten Screenshot gezeigt.
Die erste Beobachtung ist, dass jetzt die equals
Aufrufe der Person
Instanzen tatsächlich zurückkehren, true
wenn das Objekt in Bezug auf alle Attribute gleich ist, anstatt auf eine strikte Referenzgleichheit zu prüfen. Dies zeigt, dass die benutzerdefinierte equals
Implementierung auf Person
ihre Aufgabe erfüllt hat. Die zweite Beobachtung ist, dass die Implementierung der equals
Methode keinen Einfluss auf die Fähigkeit hatte, das scheinbar gleiche Objekt zum und hinzuzufügen HashSet
.
Explizite equals
und hashCode
Methoden
Es ist jetzt an der Zeit hashCode()
, der Person
Klasse eine explizite Methode hinzuzufügen . Dies hätte tatsächlich getan werden müssen, als die equals
Methode implementiert wurde. Der Grund hierfür ist in der Dokumentation zur Object.equals(Object)
Methode angegeben:
Hier ist Person
mit einer explizit implementierten hashCode
Methode, die auf den gleichen Attributen Person
wie die equals
Methode 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 Person
Klasse mit hashCode
und equals
Methoden 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 HashSet
Jetzt 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 hashCode
Implementierung auf einem Attribut basiert, das sich ändert. In diesem Beispiel wird eine setFirstName
Methode hinzugefügt Person
und der final
Modifikator aus seinem firstName
Attribut 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 Person
wird 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.