JavaRush /Java-Blog /Random-DE /Equals- und HashCode-Verträge oder was auch immer es ist
Aleksandr Zimin
Level 1
Санкт-Петербург

Equals- und HashCode-Verträge oder was auch immer es ist

Veröffentlicht in der Gruppe Random-DE
Die überwiegende Mehrheit der Java-Programmierer weiß natürlich, dass Methoden eng miteinander verbunden equalssind hashCodeund dass es ratsam ist, beide Methoden in ihren Klassen konsistent zu überschreiben. Eine etwas kleinere Zahl weiß, warum das so ist und welche traurigen Folgen ein Verstoß gegen diese Regel haben kann. Ich schlage vor, das Konzept dieser Methoden zu betrachten, ihren Zweck zu wiederholen und zu verstehen, warum sie so miteinander verbunden sind. Ich habe diesen Artikel, wie auch den vorherigen über das Laden von Klassen, für mich selbst geschrieben, um endlich alle Details des Problems offenzulegen und nicht mehr zu Quellen Dritter zurückzukehren. Deshalb freue ich mich über konstruktive Kritik, denn wenn es irgendwo Lücken gibt, sollten diese beseitigt werden. Der Artikel erwies sich leider als ziemlich lang.

entspricht den Override-Regeln

In Java ist eine Methode equals()erforderlich, um die Tatsache zu bestätigen oder zu leugnen, dass zwei Objekte desselben Ursprungs logisch gleich sind . Das heißt, beim Vergleich zweier Objekte muss der Programmierer verstehen, ob ihre signifikanten Felder gleichwertig sind . Es ist nicht notwendig, dass alle Felder identisch sein müssen, da die Methode logische Gleichheitequals() impliziert . Manchmal besteht jedoch keine besondere Notwendigkeit, diese Methode zu verwenden. Der einfachste Weg, Probleme mit einem bestimmten Mechanismus zu vermeiden, besteht darin, ihn nicht zu verwenden. Es sollte auch beachtet werden, dass Sie, sobald Sie einen Vertrag brechen, die Kontrolle darüber verlieren, wie andere Objekte und Strukturen mit Ihrem Objekt interagieren. Und anschließend wird es sehr schwierig sein, die Fehlerursache zu finden. equals

Wann diese Methode nicht überschrieben werden sollte

  • Wenn jede Instanz einer Klasse eindeutig ist.
  • Dies gilt in größerem Maße für Klassen, die ein spezifisches Verhalten bieten und nicht für die Arbeit mit Daten konzipiert sind. So zum Beispiel die Klasse Thread. Für sie ist equalsdie Implementierung der von der Klasse bereitgestellten Methode Objectmehr als ausreichend. Ein weiteres Beispiel sind Enum-Klassen ( Enum).
  • Dabei ist die Klasse tatsächlich nicht verpflichtet, die Äquivalenz ihrer Instanzen zu bestimmen.
  • Beispielsweise java.util.Randomist es für eine Klasse überhaupt nicht erforderlich, Instanzen der Klasse miteinander zu vergleichen und festzustellen, ob sie dieselbe Folge von Zufallszahlen zurückgeben können. Ganz einfach, weil die Natur dieser Klasse ein solches Verhalten nicht einmal impliziert.
  • Wenn die Klasse, die Sie erweitern, bereits über eine eigene Implementierung der Methode verfügt equalsund das Verhalten dieser Implementierung zu Ihnen passt.
  • Beispielsweise erfolgt die Implementierung für die Klassen Set, bzw. .ListMapequalsAbstractSetAbstractListAbstractMap
  • Und schließlich besteht keine Notwendigkeit zum Überschreiben, equalswenn der Gültigkeitsbereich Ihrer Klasse privateoder ist package-privateund Sie sicher sind, dass diese Methode niemals aufgerufen wird.

entspricht Vertrag

Beim Überschreiben einer Methode equalsmuss der Entwickler die in der Java-Sprachspezifikation definierten Grundregeln einhalten.
  • Reflexivität
  • Für jeden gegebenen Wert muss xder Ausdruck zurückgeben . Gegeben - was so viel bedeutetx.equals(x)true
    x != null
  • Symmetrie
  • für alle gegebenen Werte xund sollte nur zurückgegeben werden, wenn es yzurückgegeben wird . x.equals(y)truey.equals(x)true
  • Transitivität
  • für alle gegebenen Werte und muss x, wenn „returns“ und „returns“ den Wert zurückgeben . yzx.equals(y)truey.equals(z)truex.equals(z)true
  • Konsistenz
  • für alle gegebenen Werte, xund yder wiederholte Aufruf x.equals(y)gibt den Wert des vorherigen Aufrufs dieser Methode zurück, vorausgesetzt, dass sich die zum Vergleich der beiden Objekte verwendeten Felder zwischen den Aufrufen nicht geändert haben.
  • Vergleich null
  • Für jeden gegebenen Wert muss xder Aufruf zurückgeben . x.equals(null)false

gleichbedeutend mit Vertragsbruch

Viele Klassen, beispielsweise aus dem Java Collections Framework, sind auf die Implementierung der Methode angewiesen equals(), daher sollten Sie diese nicht vernachlässigen, denn Ein Verstoß gegen den Vertrag dieser Methode kann zu einem irrationalen Betrieb der Anwendung führen, und in diesem Fall wird es ziemlich schwierig sein, den Grund zu finden. Nach dem Prinzip der Reflexivität muss jedes Objekt sich selbst äquivalent sein. Wenn dieses Prinzip verletzt wird und wir ein Objekt zur Sammlung hinzufügen und dann mit der Methode danach suchen, contains()können wir das Objekt, das wir gerade zur Sammlung hinzugefügt haben, nicht finden. Die Symmetriebedingung besagt, dass zwei beliebige Objekte gleich sein müssen, unabhängig von der Reihenfolge, in der sie verglichen werden. Wenn Sie beispielsweise eine Klasse haben, die nur ein Feld vom Typ „String“ enthält, ist es falsch, equalsdieses Feld mit einer Zeichenfolge in einer Methode zu vergleichen. Weil Bei einem umgekehrten Vergleich gibt die Methode immer den Wert zurück false.
// Нарушение симметричности
public class SomeStringify {
    private String s;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o instanceof SomeStringify) {
            return s.equals(((SomeStringify) o).s);
        }
        // нарушение симметричности, классы разного происхождения
        if (o instanceof String) {
            return s.equals(o);
        }
        return false;
    }
}
//Правильное определение метода equals
@Override
public boolean equals(Object o) {
    if (this == o) return true;
    return o instanceof SomeStringify &&
            ((SomeStringify) o).s.equals(s);
}
Aus der Transitivitätsbedingung folgt, dass, wenn zwei der drei Objekte gleich sind, in diesem Fall alle drei gleich sein müssen. Dieses Prinzip kann leicht verletzt werden, wenn es notwendig ist, eine bestimmte Basisklasse durch Hinzufügen einer sinnvollen Komponente zu erweitern . Zum Beispiel zu einer Klasse Pointmit Koordinaten xund ySie müssen die Farbe des Punktes hinzufügen, indem Sie ihn erweitern. Dazu müssen Sie eine Klasse ColorPointmit dem entsprechenden Feld deklarieren color. Wenn wir also in der erweiterten Klasse die übergeordnete Methode aufrufen equalsund in der übergeordneten Klasse davon ausgehen, dass nur Koordinaten xund verglichen werden y, werden zwei Punkte unterschiedlicher Farbe, aber mit denselben Koordinaten als gleich betrachtet, was falsch ist. In diesem Fall muss der abgeleiteten Klasse beigebracht werden, Farben zu unterscheiden. Dazu können Sie zwei Methoden verwenden. Aber das eine verstößt gegen die Regel der Symmetrie und das zweite gegen die Transitivität .
// Первый способ, нарушая симметричность
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof ColorPoint)) return false;
    return super.equals(o) && ((ColorPoint) o).color == color;
}
point.equals(colorPoint)In diesem Fall gibt der Aufruf den Wert zurück trueund der Vergleich colorPoint.equals(point)gibt zurück false, weil erwartet ein Objekt „seiner“ Klasse. Somit wird die Symmetrieregel verletzt. Die zweite Methode beinhaltet eine „blinde“ Prüfung für den Fall, dass keine Daten über die Farbe des Punktes vorliegen, d. h. wir haben die Klasse Point. Oder überprüfen Sie die Farbe, wenn Informationen darüber verfügbar sind, vergleichen Sie also ein Objekt der Klasse ColorPoint.
// Метод переопределен в классе ColorPoint
@Override
public boolean equals(Object o) {
    if (!(o instanceof Point)) return false;

    // Слепая проверка
    if (!(o instanceof ColorPoint))
        return super.equals(o);

    // Полная проверка, включая цвет точки
    return super.equals(o) && ((ColorPoint) o).color == color;
}
Das Prinzip der Transitivität wird hier wie folgt verletzt. Nehmen wir an, es gibt eine Definition der folgenden Objekte:
ColorPoint p1 = new ColorPoint(1, 2, Color.RED);
Point p2 = new Point(1, 2);
ColorPoint p3 = new ColorPoint(1, 2, Color.BLUE);
Obwohl also die Gleichheit p1.equals(p2)und erfüllt ist p2.equals(p3), p1.equals(p3)wird der Wert zurückgegeben false. Gleichzeitig sieht die zweite Methode meiner Meinung nach weniger attraktiv aus, weil In einigen Fällen ist der Algorithmus möglicherweise blind und führt den Vergleich nicht vollständig durch, und Sie wissen möglicherweise nichts davon. Ein bisschen Poesie Im Allgemeinen gibt es meines Wissens nach keine konkrete Lösung für dieses Problem. Es gibt die Meinung eines maßgeblichen Autors namens Kay Horstmann, dass Sie die Verwendung des Operators instanceofdurch einen Methodenaufruf ersetzen können getClass(), der die Klasse des Objekts zurückgibt, und bevor Sie mit dem Vergleich der Objekte selbst beginnen, stellen Sie sicher, dass sie vom gleichen Typ sind , und achten nicht auf die Tatsache ihres gemeinsamen Ursprungs. Somit werden die Regeln der Symmetrie und Transitivität erfüllt. Aber gleichzeitig steht auf der anderen Seite der Barrikade ein anderer, in weiten Kreisen nicht weniger angesehener Autor, Joshua Bloch, der glaubt, dass dieser Ansatz das Substitutionsprinzip von Barbara Liskov verletzt. Dieses Prinzip besagt, dass „aufrufender Code eine Basisklasse genauso behandeln muss wie ihre Unterklassen, ohne es zu wissen . “ Und in der von Horstmann vorgeschlagenen Lösung wird dieses Prinzip eindeutig verletzt, da es auf die Implementierung ankommt. Kurz gesagt, es ist klar, dass die Angelegenheit dunkel ist. Es sollte auch beachtet werden, dass Horstmann die Regeln für die Anwendung seines Ansatzes klarstellt und im Klartext schreibt, dass man sich beim Entwerfen von Klassen für eine Strategie entscheiden muss, und wenn Gleichheitstests nur von der Oberklasse durchgeführt werden, kann man dies durch Ausführen tun die Operation instanceof. Andernfalls müssen Sie die Methode verwenden, wenn sich die Semantik der Prüfung je nach abgeleiteter Klasse ändert und die Implementierung der Methode in der Hierarchie nach unten verschoben werden muss getClass(). Joshua Bloch wiederum schlägt vor, die Vererbung aufzugeben und die Objektzusammensetzung zu verwenden, indem er eine ColorPointKlasse in die Klasse einfügt Pointund eine Zugriffsmethode bereitstellt asPoint(), um Informationen speziell über den Punkt zu erhalten. Dadurch wird ein Verstoß gegen alle Regeln vermieden, aber meiner Meinung nach wird der Code dadurch schwieriger zu verstehen sein. Die dritte Möglichkeit besteht darin, die automatische Generierung der Equals-Methode mithilfe der IDE zu verwenden. Idea reproduziert übrigens die Horstmann-Generation und ermöglicht es Ihnen, eine Strategie für die Implementierung einer Methode in einer Oberklasse oder ihren Nachkommen zu wählen. Schließlich besagt die nächste Konsistenzregel, dass auch wenn sich die Objekte xnicht yändern, ein erneuter Aufruf x.equals(y)denselben Wert wie zuvor zurückgeben muss. Die letzte Regel lautet, dass kein Objekt gleich sein sollte null. Hier ist alles klar null– das ist Unsicherheit, ist das Objekt gleich Unsicherheit? Es ist nicht klar, d.h. false.

Allgemeiner Algorithmus zur Bestimmung von Gleichheit

  1. Überprüfen Sie die Objektreferenzen thisund Methodenparameter auf Gleichheit o.
    if (this == o) return true;
  2. Prüfen Sie, ob der Link definiert ist o, also ob er es ist null.
    Wenn in Zukunft beim Vergleich von Objekttypen der Operator verwendet wird instanceof, kann dieser Punkt übersprungen werden, da dieser Parameter falsein diesem Fall zurückgibt null instanceof Object.
  3. Vergleichen Sie Objekttypen thismithilfe oeines Operators instanceofoder einer Methode getClass()und lassen Sie sich dabei von der obigen Beschreibung und Ihrer eigenen Intuition leiten.
  4. Wenn eine Methode equalsin einer Unterklasse überschrieben wird, müssen Sie unbedingt einen Aufruf durchführensuper.equals(o)
  5. Konvertieren Sie den Parametertyp oin die erforderliche Klasse.
  6. Führen Sie einen Vergleich aller wichtigen Objektfelder durch:
    • für primitive Typen (außer floatund double) mit dem Operator==
    • Für Referenzfelder müssen Sie deren Methode aufrufenequals
    • Für Arrays können Sie die zyklische Iteration oder die Methode verwendenArrays.equals()
    • Für Typen floatund doubleist es notwendig, Vergleichsmethoden der entsprechenden Wrapper-Klassen Float.compare()und zu verwendenDouble.compare()
  7. Beantworten Sie abschließend drei Fragen: Ist die implementierte Methode symmetrisch ? Transitiv ? Vereinbart ? Die anderen beiden Prinzipien ( Reflexivität und Gewissheit ) werden in der Regel automatisch ausgeführt.

HashCode-Überschreibungsregeln

Ein Hash ist eine von einem Objekt generierte Zahl, die seinen Zustand zu einem bestimmten Zeitpunkt beschreibt. Diese Nummer wird in Java hauptsächlich in Hash-Tabellen wie verwendet HashMap. In diesem Fall muss die Hash-Funktion zum Erhalten einer Zahl basierend auf einem Objekt so implementiert werden, dass eine relativ gleichmäßige Verteilung der Elemente in der Hash-Tabelle gewährleistet ist. Und auch, um die Wahrscheinlichkeit von Kollisionen zu minimieren, wenn die Funktion für verschiedene Schlüssel denselben Wert zurückgibt.

Vertrags-HashCode

Um eine Hash-Funktion zu implementieren, definiert die Sprachspezifikation die folgenden Regeln:
  • hashCodeDer einmalige oder mehrmalige Aufruf einer Methode für dasselbe Objekt muss denselben Hashwert zurückgeben, vorausgesetzt, dass sich die an der Berechnung des Werts beteiligten Felder des Objekts nicht geändert haben.
  • Der Aufruf einer Methode hashCodefür zwei Objekte sollte immer die gleiche Zahl zurückgeben, wenn die Objekte gleich sind (der Aufruf einer Methode equalsfür diese Objekte gibt zurück true).
  • Der Aufruf einer Methode hashCodefür zwei ungleiche Objekte muss unterschiedliche Hashwerte zurückgeben. Obwohl diese Anforderung nicht zwingend ist, sollte berücksichtigt werden, dass sich ihre Umsetzung positiv auf die Leistung von Hash-Tabellen auswirkt.

Die Methoden equal und hashCode müssen gemeinsam überschrieben werden

Basierend auf den oben beschriebenen Verträgen folgt daraus, dass equalsSie beim Überschreiben der Methode in Ihrem Code immer die Methode überschreiben müssen hashCode. Da sich zwei Instanzen einer Klasse tatsächlich unterscheiden, weil sie in unterschiedlichen Speicherbereichen liegen, müssen sie nach einigen logischen Kriterien verglichen werden. Dementsprechend müssen zwei logisch äquivalente Objekte denselben Hashwert zurückgeben. Was passiert, wenn nur eine dieser Methoden überschrieben wird?
  1. equalsja hashCodeNein

    Nehmen wir an, wir haben eine Methode equalsin unserer Klasse korrekt definiert und hashCodebeschlossen, die Methode in der Klasse so zu belassen, wie sie ist Object. Dann sind die beiden Objekte aus Sicht der Methode equalslogisch gleich, während sie aus Sicht der Methode hashCodenichts gemeinsam haben. Und wenn wir ein Objekt in einer Hash-Tabelle platzieren, laufen wir Gefahr, es nicht per Schlüssel zurückzubekommen.
    Zum Beispiel so:

    Map<Point, String> m = new HashMap<>();
    m.put(new Point(1, 1),Point A);
    // pointName == null
    String pointName = m.get(new Point(1, 1));

    Offensichtlich sind das platzierte Objekt und das gesuchte Objekt zwei verschiedene Objekte, obwohl sie logisch gleich sind. Aber weil Sie haben unterschiedliche Hash-Werte, weil wir gegen den Vertrag verstoßen haben. Wir können sagen, dass wir unser Objekt irgendwo in den Eingeweiden der Hash-Tabelle verloren haben.

  2. hashCodeja equalsNein.

    Was passiert, wenn wir die Methode überschreiben hashCodeund equalsdie Implementierung der Methode von der Klasse erben Object? Wie Sie wissen, vergleicht die equalsStandardmethode einfach Zeiger auf Objekte und bestimmt, ob sie auf dasselbe Objekt verweisen. Nehmen wir an, hashCodewir haben die Methode nach allen Regeln geschrieben, also mit der IDE generiert, und sie wird für logisch identische Objekte die gleichen Hashwerte zurückgeben. Offensichtlich haben wir damit bereits einen Mechanismus zum Vergleich zweier Objekte definiert.

    Daher sollte theoretisch das Beispiel aus dem vorherigen Absatz ausgeführt werden. Aber wir werden unser Objekt immer noch nicht in der Hash-Tabelle finden können. Obwohl wir dem nahe kommen, werden wir zumindest den Hash-Tabellenkorb finden, in dem das Objekt liegen wird.

    Um erfolgreich nach einem Objekt in einer Hash-Tabelle zu suchen, kommt neben dem Vergleich der Hashwerte des Schlüssels auch die Feststellung der logischen Gleichheit des Schlüssels mit dem gesuchten Objekt zum Einsatz. Das heißt, equalses gibt keine Möglichkeit, auf das Überschreiben der Methode zu verzichten.

Allgemeiner Algorithmus zur Bestimmung des HashCodes

Hier sollten Sie sich meines Erachtens keine allzu großen Sorgen machen und die Methode in Ihrer bevorzugten IDE generieren. Denn all diese Bitverschiebungen nach rechts und links auf der Suche nach dem Goldenen Schnitt, also der Normalverteilung – das ist was für völlig störrische Kerle. Persönlich bezweifle ich, dass ich es mit der gleichen Idee besser und schneller machen kann.

Statt einer Schlussfolgerung

Wir sehen also, dass Methoden in der Java-Sprache eine genau definierte Rolle equalsspielen hashCodeund darauf ausgelegt sind, die logische Gleichheitseigenschaft zweier Objekte zu erhalten. Bei der Methode equalshat dies einen direkten Bezug zum Vergleich von Objekten, bei hashCodeeinem indirekten, wenn es beispielsweise darum geht, die ungefähre Position eines Objekts in Hashtabellen oder ähnlichen Datenstrukturen zu ermitteln Erhöhen Sie die Geschwindigkeit der Suche nach einem Objekt. Neben Verträgen gibt equalses hashCodenoch eine weitere Anforderung im Zusammenhang mit dem Vergleich von Objekten. Dies ist die Konsistenz einer compareToSchnittstellenmethode Comparablemit einer equals. Diese Anforderung verpflichtet den Entwickler, immer zurückzukehren, x.equals(y) == truewenn x.compareTo(y) == 0. Das heißt, wir sehen, dass der logische Vergleich zweier Objekte nirgendwo in der Anwendung widersprüchlich sein sollte und immer konsistent sein sollte.

Quellen

Effektives Java, zweite Ausgabe. Joshua Bloch. Kostenlose Übersetzung eines sehr guten Buches. Java, eine professionelle Bibliothek. Band 1. Grundlagen. Kay Horstmann. Etwas weniger Theorie und mehr Praxis. Aber nicht alles wird so detailliert analysiert wie bei Bloch. Obwohl es eine ähnliche Ansicht gibt, ist equal(). Datenstrukturen in Bildern. HashMap Ein äußerst nützlicher Artikel über das HashMap-Gerät in Java. Anstatt auf die Quellen zu schauen.
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION