CodeGym /Java-Blog /Random-DE /Equals- und HashCode-Methoden: Best Practices
Autor
Milan Vucic
Programming Tutor at Codementor.io

Equals- und HashCode-Methoden: Best Practices

Veröffentlicht in der Gruppe Random-DE
Hallo! Heute sprechen wir über zwei wichtige Methoden in Java: equals()und hashCode(). Dies ist nicht das erste Mal, dass wir sie treffen: Der CodeGym-Kurs beginnt mit einer kurzen Lektion zum Thema equals()– lesen Sie es, wenn Sie es vergessen oder noch nie gesehen haben ... Equals- und HashCode-Methoden: Best Practices – 1In der heutigen Lektion werden wir darüber sprechen diese Konzepte im Detail. Und glauben Sie mir, wir haben etwas zu besprechen! Aber bevor wir zum Neuen übergehen, wollen wir noch einmal auffrischen, was wir bereits behandelt haben :) Wie Sie sich erinnern, ist es normalerweise keine gute Idee, zwei Objekte mit dem Operator zu vergleichen ==, da ==er Referenzen vergleicht. Hier ist unser Beispiel mit Autos aus einer aktuellen Lektion:

public class Car {

   String model;
   int maxSpeed;

   public static void main(String[] args) {

       Car car1 = new Car();
       car1.model = "Ferrari";
       car1.maxSpeed = 300;

       Car car2 = new Car();
       car2.model = "Ferrari";
       car2.maxSpeed = 300;

       System.out.println(car1 == car2);
   }
}
Konsolenausgabe:

false
Anscheinend haben wir zwei identische CarObjekte erstellt: Die Werte der entsprechenden Felder der beiden Autoobjekte sind gleich, aber das Ergebnis des Vergleichs ist immer noch falsch. Den Grund kennen wir bereits: Die car1und- car2Referenzen verweisen auf unterschiedliche Speicheradressen, sind also nicht gleich. Aber wir wollen immer noch die beiden Objekte vergleichen, nicht zwei Referenzen. Die beste Lösung zum Vergleichen von Objekten ist die equals()Methode.

equal()-Methode

Sie erinnern sich vielleicht, dass wir diese Methode nicht von Grund auf erstellen, sondern sie überschreiben: Die equals()Methode ist in der ObjectKlasse definiert. Allerdings nützt es in seiner üblichen Form wenig:

public boolean equals(Object obj) {
   return (this == obj);
}
So equals()wird die Methode in der Klasse definiert Object. Dies ist noch einmal ein Referenzvergleich. Warum haben sie es so gemacht? Woher wissen die Entwickler der Sprache, welche Objekte in Ihrem Programm als gleichwertig gelten und welche nicht? :) Das ist der Hauptpunkt der equals()Methode – der Ersteller einer Klasse ist derjenige, der bestimmt, welche Merkmale bei der Prüfung der Gleichheit von Objekten der Klasse verwendet werden. Dann überschreiben Sie die equals()Methode in Ihrer Klasse. Wenn Sie die Bedeutung von „bestimmt welche Merkmale“ nicht ganz verstehen, betrachten wir ein Beispiel. Hier ist eine einfache Klasse, die einen Mann darstellt: Man.

public class Man {

   private String noseSize;
   private String eyesColor;
   private String haircut;
   private boolean scars;
   private int dnaCode;

public Man(String noseSize, String eyesColor, String haircut, boolean scars, int dnaCode) {
   this.noseSize = noseSize;
   this.eyesColor = eyesColor;
   this.haircut = haircut;
   this.scars = scars;
   this.dnaCode = dnaCode;
}

   // Getters, setters, etc.
}
Angenommen, wir schreiben ein Programm, das feststellen muss, ob zwei Personen eineiige Zwillinge oder nur Doppelgänger sind. Wir haben fünf Merkmale: Nasengröße, Augenfarbe, Frisur, das Vorhandensein von Narben und DNA-Testergebnisse (der Einfachheit halber stellen wir dies als ganzzahligen Code dar). Welche dieser Merkmale würden Ihrer Meinung nach es unserem Programm ermöglichen, eineiige Zwillinge zu identifizieren? Equals- und HashCode-Methoden: Best Practices – 2Eine Garantie kann natürlich nur ein DNA-Test geben. Zwei Menschen können die gleiche Augenfarbe, den gleichen Haarschnitt, die gleiche Nase und sogar Narben haben – es gibt viele Menschen auf der Welt und es gibt keine Garantie dafür, dass es da draußen keine Doppelgänger gibt. Aber wir brauchen einen zuverlässigen Mechanismus: Nur das Ergebnis eines DNA-Tests lässt uns eine genaue Aussage treffen. Was bedeutet das für unsere equals()Methode? Wir müssen es im überschreibenManKlasse unter Berücksichtigung der Anforderungen unseres Programms. Die Methode sollte das int dnaCodeFeld der beiden Objekte vergleichen. Wenn sie gleich sind, sind die Objekte gleich.

@Override
public boolean equals(Object o) {
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Ist es wirklich so einfach? Nicht wirklich. Wir haben etwas übersehen. Für unsere Objekte haben wir nur ein Feld identifiziert, das für die Feststellung der Objektgleichheit relevant ist: dnaCode. Stellen Sie sich nun vor, wir hätten nicht 1, sondern 50 relevante Felder. Und wenn alle 50 Felder zweier Objekte gleich sind, dann sind die Objekte gleich. Auch ein solches Szenario ist möglich. Das Hauptproblem besteht darin, dass die Feststellung der Gleichheit durch den Vergleich von 50 Feldern ein zeitaufwändiger und ressourcenintensiver Prozess ist. ManStellen Sie sich nun vor, dass wir zusätzlich zu unserer Klasse eine WomanKlasse mit genau denselben Feldern haben, die in existieren Man. Wenn ein anderer Programmierer unsere Klassen verwendet, könnte er oder sie problemlos Code wie diesen schreiben:

public static void main(String[] args) {
  
   Man man = new Man(........); // A bunch of parameters in the constructor

   Woman woman = new Woman(.........); // The same bunch of parameters.

   System.out.println(man.equals(woman));
}
In diesem Fall ist die Überprüfung der Feldwerte sinnlos: Wir können leicht erkennen, dass es sich um Objekte zweier unterschiedlicher Klassen handelt, es gibt also keine Möglichkeit, dass sie gleich sind! Das bedeutet, dass wir der equals()Methode eine Prüfung hinzufügen sollten, die die Klassen der verglichenen Objekte vergleicht. Gut, dass wir daran gedacht haben!

@Override
public boolean equals(Object o) {
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Aber vielleicht haben wir noch etwas anderes vergessen? Hmm... Zumindest sollten wir sicherstellen, dass wir ein Objekt nicht mit sich selbst vergleichen! Wenn die Referenzen A und B auf dieselbe Speicheradresse verweisen, handelt es sich um dasselbe Objekt, und wir müssen keine Zeit damit verschwenden, 50 Felder zu vergleichen.

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Es schadet auch nicht, eine Prüfung hinzuzufügen null: Kein Objekt kann gleich sein null. Wenn der Methodenparameter also null ist, sind zusätzliche Prüfungen sinnlos. Vor diesem Hintergrund sieht unsere equals()Methode für die ManKlasse folgendermaßen aus:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;
   Man man = (Man) o;
   return dnaCode == man.dnaCode;
}
Wir führen alle oben genannten Erstprüfungen durch. Am Ende des Tages, wenn:
  • Wir vergleichen zwei Objekte derselben Klasse
  • und die verglichenen Objekte sind nicht dasselbe Objekt
  • und das übergebene Objekt ist es nichtnull
...dann gehen wir zum Vergleich der relevanten Merkmale über. Für uns sind das die dnaCodeFelder der beiden Objekte. Beachten Sie beim Überschreiben der equals()Methode unbedingt die folgenden Anforderungen:
  1. Reflexivität.

    Wenn die equals()Methode verwendet wird, um ein beliebiges Objekt mit sich selbst zu vergleichen, muss sie „true“ zurückgeben.
    Dieser Vorgabe sind wir bereits nachgekommen. Unsere Methode beinhaltet:

    
    if (this == o) return true;
    

  2. Symmetrie.

    Wenn a.equals(b) == true, dann b.equals(a)muss zurückgegeben werden true.
    Unsere Methode erfüllt auch diese Anforderung.

  3. Transitivität.

    Wenn zwei Objekte einem dritten Objekt gleich sind, müssen sie einander gleich sein.
    Wenn a.equals(b) == trueund a.equals(c) == true, dann b.equals(c)muss auch true zurückgegeben werden.

  4. Beharrlichkeit.

    Das Ergebnis von equals()darf sich nur ändern, wenn die beteiligten Felder geändert werden. Wenn sich die Daten der beiden Objekte nicht ändern, muss das Ergebnis equals()immer das gleiche sein.

  5. Ungleichheit mit null.

    Für jedes Objekt a.equals(null)muss „false“ zurückgegeben werden
    . Hierbei handelt es sich nicht nur um eine Reihe „nützlicher Empfehlungen“, sondern um einen strengen Vertrag , der in der Oracle-Dokumentation festgelegt ist

hashCode()-Methode

Lassen Sie uns nun über die hashCode()Methode sprechen. Warum ist es notwendig? Für genau den gleichen Zweck – zum Vergleichen von Objekten. Aber das haben wir schon equals()! Warum eine andere Methode? Die Antwort ist einfach: um die Leistung zu verbessern. Eine Hash-Funktion, die in Java mithilfe der hashCode()Methode dargestellt wird, gibt für jedes Objekt einen numerischen Wert fester Länge zurück. In Java gibt die hashCode()Methode für jedes Objekt eine 32-Bit-Zahl ( int) zurück. Der Vergleich zweier Zahlen ist mit dieser Methode viel schneller als der Vergleich zweier Objekte equals(), insbesondere wenn diese Methode viele Felder berücksichtigt. Wenn unser Programm Objekte vergleicht, geht das viel einfacher über einen Hash-Code. Nur wenn die Objekte basierend auf der hashCode()Methode gleich sind, wird der Vergleich fortgesetztequals()Methode. So funktionieren übrigens Hash-basierte Datenstrukturen, zum Beispiel das bekannte HashMap! Die hashCode()Methode equals()wird ebenso wie die Methode vom Entwickler überschrieben. Und genau wie equals()für die hashCode()Methode gelten offizielle Anforderungen, die in der Oracle-Dokumentation aufgeführt sind:
  1. Wenn zwei Objekte gleich sind (dh die equals()Methode gibt „true“ zurück), müssen sie denselben Hash-Code haben.

    Andernfalls wären unsere Methoden bedeutungslos. Wie oben erwähnt, hashCode()sollte zunächst eine Überprüfung durchgeführt werden, um die Leistung zu verbessern. Wenn die Hash-Codes unterschiedlich wären, würde die Prüfung „false“ zurückgeben, obwohl die Objekte tatsächlich gleich sind, je nachdem, wie wir die equals()Methode definiert haben.

  2. Wenn die hashCode()Methode mehrmals für dasselbe Objekt aufgerufen wird, muss sie jedes Mal dieselbe Nummer zurückgeben.

  3. Regel 1 funktioniert nicht in die entgegengesetzte Richtung. Zwei verschiedene Objekte können denselben Hash-Code haben.

Die dritte Regel ist etwas verwirrend. Wie kann das sein? Die Erklärung ist ganz einfach. Die hashCode()Methode gibt eine zurück int. An intist eine 32-Bit-Zahl. Der Wertebereich ist begrenzt: von -2.147.483.648 bis +2.147.483.647. Mit anderen Worten: Es gibt knapp über 4 Milliarden mögliche Werte für eine int. Stellen Sie sich nun vor, Sie erstellen ein Programm zum Speichern von Daten über alle auf der Erde lebenden Menschen. Jede Person entspricht ihrem eigenen PersonObjekt (ähnlich der ManKlasse). Auf dem Planeten leben etwa 7,5 Milliarden Menschen. Mit anderen Worten, egal wie clever der Algorithmus ist, den wir für die Konvertierung schreibenPersonObjekte zu einem int hinzufügen, haben wir einfach nicht genügend mögliche Zahlen. Wir haben nur 4,5 Milliarden mögliche int-Werte, aber es gibt noch viel mehr Menschen. Das bedeutet, dass, egal wie sehr wir es versuchen, einige verschiedene Leute die gleichen Hash-Codes haben werden. Wenn dies geschieht (Hash-Codes stimmen für zwei verschiedene Objekte überein), nennen wir es eine Kollision. Beim Überschreiben der hashCode()Methode besteht eines der Ziele des Programmierers darin, die potenzielle Anzahl von Kollisionen zu minimieren. Wie wird die Methode in der Klasse unter Berücksichtigung all dieser Regeln hashCode()aussehen Person? So was:

@Override
public int hashCode() {
   return dnaCode;
}
Überrascht? :) Schaut man sich die Auflagen an, sieht man, dass wir sie alle einhalten. Objekte, für die unsere equals()Methode „true“ zurückgibt, sind gemäß ebenfalls gleich hashCode(). Wenn unsere beiden PersonObjekte gleich sind equals(das heißt, sie haben die gleichen dnaCode), dann gibt unsere Methode dieselbe Zahl zurück. Betrachten wir ein schwierigeres Beispiel. Angenommen, unser Programm soll Luxusautos für Autosammler auswählen. Sammeln kann ein komplexes Hobby mit vielen Besonderheiten sein. Ein bestimmtes Auto von 1963 kann 100-mal mehr kosten als ein Auto von 1964. Ein rotes Auto von 1970 kann 100-mal mehr kosten als ein blaues Auto derselben Marke aus demselben Jahr. Equals- und HashCode-Methoden: Best Practices – 4In unserem vorherigen Beispiel mit der PersonKlasse haben wir die meisten Felder (d. h. menschliche Merkmale) als unbedeutend verworfen und nur die Felder verwendetdnaCodeFeld in Vergleichen. Wir bewegen uns jetzt in einem sehr eigenwilligen Bereich, in dem es keine unbedeutenden Details gibt! Hier ist unsere LuxuryAutoKlasse:

public class LuxuryAuto {

   private String model;
   private int manufactureYear;
   private int dollarPrice;

   public LuxuryAuto(String model, int manufactureYear, int dollarPrice) {
       this.model = model;
       this.manufactureYear = manufactureYear;
       this.dollarPrice = dollarPrice;
   }

   // ...getters, setters, etc.
}
Jetzt müssen wir alle Felder in unseren Vergleichen berücksichtigen. Jeder Fehler könnte einen Kunden Hunderttausende Dollar kosten, daher wäre es besser, besonders auf Nummer sicher zu gehen:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   if (dollarPrice != that.dollarPrice) return false;
   return model.equals(that.model);
}
Bei unserer equals()Methode haben wir nicht alle Kontrollen vergessen, über die wir zuvor gesprochen haben. Aber jetzt vergleichen wir jedes der drei Felder unserer Objekte. Für dieses Programm benötigen wir absolute Gleichheit, dh Gleichheit aller Felder. Was ist mit hashCode?

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Das modelFeld in unserer Klasse ist ein String. Dies ist praktisch, da die StringKlasse die Methode bereits überschreibt hashCode(). Wir berechnen den modelHash-Code des Feldes und addieren dann die Summe der beiden anderen numerischen Felder dazu. Java-Entwickler haben einen einfachen Trick, mit dem sie die Anzahl der Kollisionen reduzieren: Bei der Berechnung eines Hash-Codes multiplizieren sie das Zwischenergebnis mit einer ungeraden Primzahl. Die am häufigsten verwendete Zahl ist 29 oder 31. Wir werden uns jetzt nicht mit den mathematischen Feinheiten befassen, denken aber in Zukunft daran, dass die Multiplikation von Zwischenergebnissen mit einer ausreichend großen ungeraden Zahl dazu beiträgt, die Ergebnisse der Hash-Funktion zu „verteilen“ und Reduzieren Sie daher die Anzahl der Objekte mit demselben Hash-Code. Für unsere hashCode()Methode in LuxuryAuto würde es so aussehen:

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Weitere Informationen zu allen Feinheiten dieses Mechanismus finden Sie in diesem Beitrag auf StackOverflow sowie im Buch Effective Java von Joshua Bloch. Abschließend noch ein wichtiger Punkt, der erwähnenswert ist. Jedes Mal, wenn wir die Methode equals()„and“ überschrieben haben hashCode(), haben wir bestimmte Instanzfelder ausgewählt, die in diesen Methoden berücksichtigt werden. Diese Methoden berücksichtigen dieselben Felder. equals()Aber können wir verschiedene Bereiche in und berücksichtigen hashCode()? Technisch gesehen können wir das. Aber das ist eine schlechte Idee, und hier ist der Grund:

@Override
public boolean equals(Object o) {
   if (this == o) return true;
   if (o == null || getClass() != o.getClass()) return false;

   LuxuryAuto that = (LuxuryAuto) o;

   if (manufactureYear != that.manufactureYear) return false;
   return dollarPrice == that.dollarPrice;
}

@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Hier sind unsere equals()und hashCode()Methoden für die LuxuryAutoKlasse. Die hashCode()Methode blieb unverändert, aber wir haben das modelFeld aus der equals()Methode entfernt. Das Modell ist kein Merkmal mehr, das verwendet wird, wenn die equals()Methode zwei Objekte vergleicht. Bei der Berechnung des Hash-Codes wird dieses Feld jedoch weiterhin berücksichtigt. Was bekommen wir als Ergebnis? Lasst uns zwei Autos bauen und es herausfinden!

public class Main {

   public static void main(String[] args) {

       LuxuryAuto ferrariGTO = new LuxuryAuto("Ferrari 250 GTO", 1963, 70000000);
       LuxuryAuto ferrariSpider = new LuxuryAuto("Ferrari 335 S Spider Scaglietti", 1963, 70000000);

       System.out.println("Are these two objects equal to each other?");
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println("What are their hash codes?");
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Are these two objects equal to each other? 
true 
What are their hash codes? 
-1372326051 
1668702472
Fehler! Durch die Verwendung unterschiedlicher Felder für die equals()und hashCode()-Methoden haben wir gegen die für sie festgelegten Verträge verstoßen! Zwei nach der Methode gleiche Objekte equals()müssen den gleichen Hashcode haben. Wir haben dafür unterschiedliche Werte erhalten. Solche Fehler können zu absolut unglaublichen Konsequenzen führen, insbesondere wenn mit Sammlungen gearbeitet wird, die einen Hash verwenden. Daher sollten Sie beim Überschreiben equals()von und hashCode()dieselben Felder berücksichtigen. Diese Lektion war ziemlich lang, aber Sie haben heute viel gelernt! :) Jetzt geht es wieder ans Lösen von Aufgaben!
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION