JavaRush /Java-Blog /Random-DE /Equals- und HashCode-Methoden: Anwendungspraxis

Equals- und HashCode-Methoden: Anwendungspraxis

Veröffentlicht in der Gruppe Random-DE
Hallo! Heute werden wir über zwei wichtige Methoden in Java sprechen – equals()und hashCode(). Dies ist nicht das erste Mal, dass wir sie treffen: Zu Beginn des JavaRush-Kurses gab es einen kurzen Vortrag zum Thema equals()– lesen Sie ihn, wenn Sie ihn vergessen oder noch nicht gesehen haben. Methoden gleich &  hashCode: Nutzungspraxis - 1In der heutigen Lektion werden wir ausführlich über diese Konzepte sprechen – glauben Sie mir, es gibt viel zu besprechen! Und bevor wir zu etwas Neuem übergehen, wollen wir unsere Erinnerung an das, was wir bereits behandelt haben, auffrischen :) Wie Sie sich erinnern, ist der übliche Vergleich zweier Objekte mit dem ==Operator „ “ eine schlechte Idee, da „ ==“ Referenzen vergleicht. Hier ist unser Beispiel mit Autos aus einem aktuellen Vortrag:
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
Es scheint, dass wir zwei identische Objekte der Klasse erstellt haben Car: Alle Felder auf den beiden Maschinen sind gleich, aber das Ergebnis des Vergleichs ist immer noch falsch. Den Grund kennen wir bereits: Die Links car1und car2verweisen auf unterschiedliche Adressen im Speicher, sind also nicht gleich. Wir wollen immer noch zwei Objekte vergleichen, nicht zwei Referenzen. Die beste Lösung zum Vergleichen von Objekten ist die equals().

equal()-Methode

Sie erinnern sich vielleicht, dass wir diese Methode nicht von Grund auf erstellen, sondern überschreiben – schließlich equals()ist die Methode in der Klasse definiert Object. In seiner üblichen Form nützt es jedoch wenig:
public boolean equals(Object obj) {
   return (this == obj);
}
So equals()wird die Methode in der Klasse definiert Object. Der gleiche Linkvergleich. Warum wurde er so gemacht? Nun, woher wissen die Entwickler der Sprache, welche Objekte in Ihrem Programm als gleichwertig gelten und welche nicht? :) Das ist die Grundidee der Methode equals()– der Ersteller der Klasse bestimmt selbst die Merkmale, anhand derer die Gleichheit von Objekten dieser Klasse überprüft wird. Dadurch überschreiben Sie die Methode equals()in Ihrer Klasse. Wenn Sie die Bedeutung von „Sie definieren die Merkmale selbst“ nicht ganz verstehen, schauen wir uns ein Beispiel an. Hier ist eine einfache Klasse von Personen - 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;
}

   //Getter, Setter usw.
}
Nehmen wir an, wir schreiben ein Programm, das feststellen muss, ob zwei Personen durch Zwillinge oder nur durch Doppelgänger verwandt sind. Wir verfügen über fünf Merkmale: Nasengröße, Augenfarbe, Frisur, Vorhandensein von Narben und die Ergebnisse eines biologischen DNA-Tests (der Einfachheit halber in Form einer Codenummer). Welche dieser Merkmale ermöglichen es unserem Programm Ihrer Meinung nach, Zwillingsverwandte zu identifizieren? Methoden gleich &  hashCode: Nutzungspraxis – 2Eine Garantie kann natürlich nur ein biologischer Test geben. Zwei Menschen können die gleiche Augenfarbe, Frisur, Nase und sogar Narben haben – es gibt viele Menschen auf der Welt und es ist unmöglich, Zufälle zu vermeiden. Wir brauchen einen zuverlässigen Mechanismus: Nur das Ergebnis eines DNA-Tests lässt uns eine genaue Schlussfolgerung zu. Was bedeutet das für unsere Methode equals()? Wir müssen es in einer Klasse Manunter Berücksichtigung der Anforderungen unseres Programms neu definieren. Die Methode muss das Feld int dnaCodezweier 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 verpasst. In diesem Fall haben wir für unsere Objekte nur ein „signifikantes“ Feld definiert, durch das ihre Gleichheit festgestellt wird – dnaCode. Stellen Sie sich nun vor, wir hätten nicht 1, sondern 50 solcher „signifikanten“ Felder. Und wenn alle 50 Felder zweier Objekte gleich sind, dann sind die Objekte gleich. Das könnte auch passieren. Das Hauptproblem besteht darin, dass die Berechnung der Gleichheit von 50 Feldern ein zeitaufwändiger und ressourcenintensiver Prozess ist. Stellen Sie sich nun vor, dass wir zusätzlich zur Klasse Maneine Klasse Womanmit genau denselben Feldern wie in haben Man. Und wenn ein anderer Programmierer Ihre Klassen verwendet, kann er in sein Programm problemlos so etwas schreiben wie:
public static void main(String[] args) {

   Man man = new Man(........); //eine Reihe von Parametern im Konstruktor

   Woman woman = new Woman(.........);//gleicher Parametersatz.

   System.out.println(man.equals(woman));
}
In diesem Fall macht es keinen Sinn, die Feldwerte zu überprüfen: Wir sehen, dass es sich um Objekte zweier verschiedener Klassen handelt, die grundsätzlich nicht gleich sein können! Das bedeutet, dass wir in der Methode ein Häkchen setzen müssen equals()– einen Vergleich von Objekten zweier identischer Klassen. 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 vergessen? Hmm... Zumindest sollten wir sicherstellen, dass wir das Objekt nicht mit sich selbst vergleichen! Wenn die Referenzen A und B auf dieselbe Adresse im Speicher verweisen, handelt es sich um dasselbe Objekt, und wir müssen auch keine Zeit mit dem Vergleich von 50 Feldern verschwenden.
@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;
}
Darüber hinaus würde es nicht schaden, eine Prüfung für hinzuzufügen null: Kein Objekt kann gleich sein null. In diesem Fall machen zusätzliche Prüfungen keinen Sinn. Unter Berücksichtigung all dessen sieht unsere equals()Klassenmethode Manfolgendermaß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. Wenn sich herausstellt, dass:
  • Wir vergleichen zwei Objekte derselben Klasse
  • Dies ist nicht dasselbe Objekt
  • Wir vergleichen unser Objekt nicht mitnull
...dann geht es weiter zum Vergleich wesentlicher Merkmale. In unserem Fall die Felder dnaCodezweier Objekte. Stellen Sie beim Überschreiben einer Methode equals()sicher, dass Sie die folgenden Anforderungen erfüllen:
  1. Reflexivität.

    Jedes Objekt muss equals()für sich selbst sein.
    Dieser Anforderung haben wir bereits Rechnung getragen. Unsere Methode besagt:

    if (this == o) return true;

  2. Symmetrie.

    Wenn a.equals(b) == true, dann b.equals(a)sollte es zurückkehren 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 sollte die Prüfung b.equals(c)auch true zurückgeben.

  4. Dauerhaftigkeit.

    Die Ergebnisse der Arbeit equals()sollten sich nur ändern, wenn sich die darin enthaltenen Felder ändern. Wenn sich die Daten zweier Objekte nicht geändert haben, sollten die Ergebnisse der Prüfung equals()immer gleich sein.

  5. Ungleichheit mit null.

    Für jedes Objekt a.equals(null)muss die Prüfung „false“ zurückgeben.
    Hierbei handelt es sich nicht nur um eine Reihe „nützlicher Empfehlungen“, sondern um einen strengen Methodenvertrag , der in der Oracle-Dokumentation vorgeschrieben ist

hashCode()-Methode

Lassen Sie uns nun über die Methode sprechen hashCode(). Warum wird es benötigt? Genau dem gleichen Zweck – dem Vergleichen von Objekten. Aber wir haben es schon equals()! Warum eine andere Methode? Die Antwort ist einfach: die Produktivität zu verbessern. Eine Hash-Funktion, die in Java durch die Methode , dargestellt wird hashCode(), gibt einen numerischen Wert fester Länge für jedes Objekt zurück. Im Fall von Java gibt die Methode hashCode()eine 32-Bit-Zahl vom Typ zurück int. Der Vergleich zweier Zahlen miteinander ist viel schneller als der Vergleich zweier Objekte mit der Methode equals(), insbesondere wenn viele Felder verwendet werden. Wenn unser Programm Objekte vergleicht, ist es viel einfacher, dies über den Hash-Code zu tun, und nur wenn sie gleich sind hashCode(), fahren Sie mit dem Vergleich fort equals(). So funktionieren übrigens Hash-basierte Datenstrukturen – zum Beispiel die, die Sie kennen HashMap! Die Methode wird hashCode(), genau wie equals(), vom Entwickler selbst überschrieben. Und genau wie für gelten für equals()die Methode hashCode()offizielle Anforderungen, die in der Oracle-Dokumentation festgelegt sind:
  1. Wenn zwei Objekte gleich sind (d. h. die Methode equals()gibt „true“ zurück), müssen sie denselben Hash-Code haben.

    Sonst sind unsere Methoden bedeutungslos. hashCode()Wie bereits erwähnt, sollte die Überprüfung durch an erster Stelle stehen, um die Leistung zu verbessern. Wenn die Hash-Codes unterschiedlich sind, gibt die Prüfung „false“ zurück, obwohl die Objekte tatsächlich gleich sind (wie wir in der Methode definiert haben equals()).

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

  3. Regel 1 funktioniert nicht umgekehrt. 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 Methode hashCode()gibt zurück int. intist eine 32-Bit-Zahl. Die Anzahl der Werte ist begrenzt – von -2.147.483.648 bis +2.147.483.647. Mit anderen Worten, es gibt etwas mehr als 4 Milliarden Variationen der Zahl int. Stellen Sie sich nun vor, Sie erstellen ein Programm zum Speichern von Daten über alle lebenden Menschen auf der Erde. Jede Person hat ihr eigenes Klassenobjekt Man. Auf der Erde leben etwa 7,5 Milliarden Menschen. Mit anderen Worten: Egal wie gut der Algorithmus ist, Manden wir schreiben, um Objekte in Zahlen umzuwandeln, wir werden einfach nicht genug Zahlen haben. Wir haben nur 4,5 Milliarden Optionen und viel mehr Menschen. Das bedeutet, dass die Hash-Codes für verschiedene Personen gleich sein werden, egal wie sehr wir uns bemühen. Diese Situation (die Hash-Codes zweier verschiedener Objekte stimmen überein) wird als Kollision bezeichnet. Eines der Ziele des Programmierers beim Überschreiben einer Methode hashCode()besteht darin, die potenzielle Anzahl von Kollisionen so weit wie möglich zu reduzieren. Wie wird unsere Methode hashCode()für die Klasse Manunter Berücksichtigung all dieser Regeln aussehen? So:
@Override
public int hashCode() {
   return dnaCode;
}
Überrascht? :) Unerwartet, aber wenn man sich die Auflagen anschaut, sieht man, dass wir alles einhalten. Objekte, für die unser Wert equals()true zurückgibt, sind in gleich hashCode(). Wenn unsere beiden Objekte Manden gleichen Wert haben equals(d. h. sie haben den gleichen Wert dnaCode), gibt unsere Methode dieselbe Zahl zurück. Schauen wir uns ein komplizierteres Beispiel an. Nehmen wir an, unser Programm soll Luxusautos für Sammlerkunden auswählen. Sammeln ist eine komplexe Sache und hat viele Besonderheiten. Ein Auto aus dem Jahr 1963 kann 100-mal teurer sein als das gleiche Auto aus dem Jahr 1964. Ein rotes Auto aus dem Jahr 1970 kann 100-mal mehr kosten als ein blaues Auto derselben Marke aus demselben Jahr. Methoden gleich &  hashCode: Nutzungspraxis – 4Im ersten Fall Manhaben wir mit der Klasse die meisten Felder (also Personenmerkmale) als unbedeutend verworfen und nur das Feld zum Vergleich herangezogen dnaCode. Hier arbeiten wir mit einem sehr einzigartigen Bereich, und es darf keine Kleinigkeiten geben! Hier ist unsere Klasse LuxuryAuto:
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;
   }

   //... Getter, Setter usw.
}
Hier müssen wir beim Vergleich alle Felder berücksichtigen. Jeder Fehler kann den Kunden Hunderttausende Dollar kosten, daher ist es besser, auf der sicheren Seite zu sein:
@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 Methode equals()haben wir alle Kontrollen nicht vergessen, über die wir zuvor gesprochen haben. Aber jetzt vergleichen wir jedes der drei Felder unserer Objekte. In diesem Programm muss in allen Bereichen absolute Gleichheit herrschen. Wie wäre es mit hashCode?
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = result + manufactureYear;
   result = result + dollarPrice;
   return result;
}
Das Feld modelin unserer Klasse ist eine Zeichenfolge. Das ist praktisch: StringDie Methode hashCode()ist in der Klasse bereits überschrieben. Wir berechnen den Hash-Code des Feldes modelund addieren dazu die Summe der beiden anderen numerischen Felder. Um die Anzahl der Kollisionen zu reduzieren, gibt es in Java einen kleinen Trick: Bei der Berechnung des Hash-Codes multipliziert man das Zwischenergebnis mit einer ungeraden Primzahl. Die am häufigsten verwendete Zahl ist 29 oder 31. Wir werden jetzt nicht auf die Details der Mathematik eingehen, aber denken Sie zum späteren Nachschlagen daran, dass die Multiplikation von Zwischenergebnissen mit einer ausreichend großen ungeraden Zahl dabei hilft, die Ergebnisse des Hashs zu „verteilen“. Funktion und am Ende gibt es weniger Objekte mit demselben Hashcode. Für unsere Methode hashCode()in LuxuryAuto sieht es so aus:
@Override
public int hashCode() {
   int result = model == null ? 0 : model.hashCode();
   result = 31 * result + manufactureYear;
   result = 31 * result + dollarPrice;
   return result;
}
Mehr über alle Feinheiten dieses Mechanismus können Sie in diesem Beitrag auf StackOverflow sowie in Joshua Blochs Buch „ Effective Java “ lesen. Abschließend gibt es noch einen wichtigen Punkt, der erwähnt werden sollte. Bei jedem Überschreiben equals()haben hashCode()wir bestimmte Felder des Objekts ausgewählt, die in diesen Methoden berücksichtigt wurden. 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 Methoden equals()für hashCode()die LuxuryAuto-Klasse. Die Methode hashCode()blieb unverändert und equals()wir haben das Feld aus der Methode entfernt model. Nun ist das Modell kein Merkmal zum Vergleich zweier Objekte equals(). Es wird aber dennoch bei der Berechnung des Hash-Codes berücksichtigt. Was bekommen wir als Ergebnis? Lasst uns zwei Autos erstellen und es uns ansehen!
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(„Sind diese beiden Objekte einander gleich?“);
       System.out.println(ferrariGTO.equals(ferrariSpider));

       System.out.println(„Was sind ihre Hash-Codes?“);
       System.out.println(ferrariGTO.hashCode());
       System.out.println(ferrariSpider.hashCode());
   }
}

Эти два ein Objektа равны друг другу?
true
Какие у них хэш-Codeы?
-1372326051
1668702472
Fehler! Indem wir unterschiedliche Felder für verwendet haben, equals()haben hashCode()wir gegen den dafür geschlossenen Vertrag verstoßen! Zwei gleiche equals()Objekte müssen den gleichen Hash-Code haben. Wir haben unterschiedliche Bedeutungen für sie. Solche Fehler können zu den unglaublichsten Konsequenzen führen, insbesondere wenn mit Sammlungen gearbeitet wird, die Hashes verwenden. Daher ist es bei der Neudefinition korrekt, dieselben Felder zu verwenden equals(). hashCode()Der Vortrag ist ziemlich lang geworden, aber heute hast du viel Neues gelernt! :) Es ist Zeit, wieder Probleme zu lösen!
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION