Java equals () und hashCode () Verträge

Java entspricht () und hashCode () Verträgen

1. Überblick

In diesem Tutorial werden zwei Methoden vorgestellt, die eng zusammengehören:equals() undhashCode(). Wir werden uns auf ihre Beziehung zueinander konzentrieren, wie man sie richtig überschreibt und warum wir beide oder keine überschreiben sollten.

2. equals()

DieObject-Klasse definiert sowohl dieequals()- als auch diehashCode()-Methode. Dies bedeutet, dass diese beiden Methoden in jeder Java-Klasse implizit definiert sind, einschließlich der von uns erstellten:

class Money {
    int amount;
    String currencyCode;
}
Money income = new Money(55, "USD");
Money expenses = new Money(55, "USD");
boolean balanced = income.equals(expenses)

Wir würden erwarten, dassincome.equals(expenses)true zurückgibt. Mit derMoney-Klasse in ihrer aktuellen Form wird dies jedoch nicht der Fall sein.

Die Standardimplementierung vonequals() in der KlasseObject besagt, dass Gleichheit mit Objektidentität identisch ist. Undincome undexpenses sind zwei verschiedene Instanzen.

2.1. equals() überschreiben

Überschreiben wir dieequals()-Methode, damit nicht nur die Objektidentität, sondern auch der Wert der beiden relevanten Eigenschaften berücksichtigt wird:

@Override
public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Money))
        return false;
    Money other = (Money)o;
    boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
      || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
    return this.amount == other.amount && currencyCodeEquals;
}

2.2. equals() Vertrag

Java SE definiert einen Vertrag, den unsere Implementierung derequals()-Methode erfüllen muss. Most of the criteria are common sense. Die Methodeequals() muss sein:

  • reflexive: Ein Objekt muss sich selbst entsprechen

  • symmetric:x.equals(y) must return the same result as y.equals(x)

  • transitive: Wennx.equals(y) undy.equals(z), dann auchx.equals(z)

  • consistent: Der Wert vonequals() sollte sich nur ändern, wenn sich eine inequals() enthaltene Eigenschaft ändert (keine Zufälligkeit zulässig).

Wir können die genauen Kriterien inJava SE Docs for the Object class nachschlagen.

2.3. Verletzung der Symmetrie vonequals()mit der Vererbung

Wenn das Kriterium fürequals() so vernünftig ist, wie können wir es überhaupt verletzen? Nun,violations happen most often, if we extend a class that has overridden equals(). Betrachten wir eineVoucher-Klasse, die unsereMoney-Klasse erweitert:

class WrongVoucher extends Money {

    private String store;

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof WrongVoucher))
            return false;
        WrongVoucher other = (WrongVoucher)o;
        boolean currencyCodeEquals = (this.currencyCode == null && other.currencyCode == null)
          || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
        boolean storeEquals = (this.store == null && other.store == null)
          || (this.store != null && this.store.equals(other.store));
        return this.amount == other.amount && currencyCodeEquals && storeEquals;
    }

    // other methods
}

Auf den ersten Blick scheinen die KlasseVoucherund ihre Überschreibung fürequals()korrekt zu sein. Und beide Methoden vonequals()verhalten sich korrekt, solange wirMoney mitMoney oderVoucher mitVoucher vergleichen. But what happens, if we compare these two objects?

Money cash = new Money(42, "USD");
WrongVoucher voucher = new WrongVoucher(42, "USD", "Amazon");

voucher.equals(cash) => false // As expected.
cash.equals(voucher) => true // That's wrong.

Dies verstößt gegen die Symmetriekriterien desequals()-Vertrags.

2.4. Festlegen der Symmetrie vonequals()mit der Komposition

Um diese Gefahr zu vermeiden, sollten wir favor composition over inheritance.

AnstattMoney zu unterklassifizieren, erstellen wir eineVoucher-Klasse mit einerMoney-Eigenschaft:

class Voucher {

    private Money value;
    private String store;

    Voucher(int amount, String currencyCode, String store) {
        this.value = new Money(amount, currencyCode);
        this.store = store;
    }

    @Override
    public boolean equals(Object o) {
        if (o == this)
            return true;
        if (!(o instanceof Voucher))
            return false;
        Voucher other = (Voucher) o;
        boolean valueEquals = (this.value == null && other.value == null)
          || (this.value != null && this.value.equals(other.value));
        boolean storeEquals = (this.store == null && other.store == null)
          || (this.store != null && this.store.equals(other.store));
        return valueEquals && storeEquals;
    }

    // other methods
}

Und jetzt arbeitenequals will symmetrisch, wie es der Vertrag erfordert.

3. hashCode()

hashCode() gibt eine Ganzzahl zurück, die die aktuelle Instanz der Klasse darstellt. Wir sollten diesen Wert in Übereinstimmung mit der Definition der Gleichheit für die Klasse berechnen. Daher müssen wir mitif we override the equals() auchhashCode() überschreiben.

Weitere Informationen finden Sie in unserenguide to hashCode().

3.1. hashCode() Vertrag

Java SE definiert auch einen Vertrag für diehashCode()-Methode. Ein genauer Blick darauf zeigt, wie enghashCode() undequals() verwandt sind.

Alle drei Kriterien im Vertrag vonhashCode()erwähnen in gewisser Weise die Methode vonequals():

  • internal consistency: Der Wert vonhashCode() darf sich nur ändern, wenn sich eine Eigenschaft inequals() ändert

  • equals consistency: * Objekte, die einander gleich sind, müssen denselben Hashcode zurückgeben *

  • collisions:unequal objects may have the same hashCode

3.2. Verletzung der Konsistenz vonhashCode() undequals()

Das zweite Kriterium des HashCode-Methodenvertrags hat eine wichtige Konsequenz:If we override equals(), we must also override hashCode(). Und dies ist bei weitem die am weitesten verbreitete Verletzung in Bezug auf die Verträge der Methodenequals() undhashCode().

Sehen wir uns ein solches Beispiel an:

class Team {

    String city;
    String department;

    @Override
    public final boolean equals(Object o) {
        // implementation
    }
}

Die KlasseTeam überschreibt nurequals(), verwendet jedoch implizit die Standardimplementierung vonhashCode(), wie in der KlasseObject definiert. Und dies gibt für jede Instanz der Klasse ein andereshashCode() zurück. This violates the second rule.

Wenn wir nun zweiTeam Objekte erstellen, beide mit der Stadt „New York“ und der Abteilung „Marketing“, sind sie gleich,but they will return different hashCodes.

3.3. HashMap Schlüssel mit inkonsistentenhashCode()

Aber warum ist die Vertragsverletzung in unsererTeam-Klasse ein Problem? Nun, der Ärger beginnt, wenn einige Hash-basierte Sammlungen beteiligt sind. Versuchen wir, unsere KlasseTeamals Schlüssel fürHashMapzu verwenden:

Map leaders = new HashMap<>();
leaders.put(new Team("New York", "development"), "Anne");
leaders.put(new Team("Boston", "development"), "Brian");
leaders.put(new Team("Boston", "marketing"), "Charlie");

Team myTeam = new Team("New York", "development");
String myTeamLeader = leaders.get(myTeam);

Wir würden erwarten, dassmyTeamLeader "Anne" zurückgibt. But with the current code, it doesn’t.

Wenn wir Instanzen der KlasseTeamals SchlüsselHashMapverwenden möchten, müssen wir die MethodehashCode()überschreiben, damit sie dem Vertrag entspricht:Equal objects return the same hashCode.

Sehen wir uns eine Beispielimplementierung an:

@Override
public final int hashCode() {
    int result = 17;
    if (city != null) {
        result = 31 * result + city.hashCode();
    }
    if (department != null) {
        result = 31 * result + department.hashCode();
    }
    return result;
}

Nach dieser Änderung gibtleaders.get(myTeam) wie erwartet "Anne" zurück.

4. Wann überschreiben wirequals() undhashCode()?

Generally, we want to override either both of them or neither of them. Wir haben gerade in Abschnitt 3 die unerwünschten Folgen gesehen, wenn wir diese Regel ignorieren.

Domain-Driven Design kann uns bei der Entscheidung helfen, wann wir die Umstände verlassen sollten. Für Entitätsklassen - für Objekte mit einer eigenen Identität - ist die Standardimplementierung oft sinnvoll.

for value objects, we usually prefer equality based on their properties. Daher möchten Sieequals() undhashCode() überschreiben. Denken Sie daran, dass unsereMoney-Klasse aus Abschnitt 2: 55 USD 55 USD entspricht - auch wenn es sich um zwei separate Instanzen handelt.

5. Implementierungshelfer

Wir schreiben die Implementierung dieser Methoden normalerweise nicht von Hand. Wie man sieht, gibt es einige Fallstricke.

Ein üblicher Weg besteht darin,let our IDE die Methodenequals() undhashCode() zu generieren.

Apache Commons Lang undGoogle Guava haben Hilfsklassen, um das Schreiben beider Methoden zu vereinfachen.

Project Lombok liefert auch eine Annotation von@EqualsAndHashCode. Note again how equals() and hashCode() “go together” and even have a common annotation.

6. Überprüfung der Verträge

Wenn wir überprüfen möchten, ob unsere Implementierungen den Java SE-Verträgen und einigen Best Practices entsprechen,we can use the EqualsVerifier library.

Fügen wir die Maven-Testabhängigkeit vonEqualsVerifierhinzu:


    nl.jqno.equalsverifier
    equalsverifier
    3.0.3
    test

Lassen Sie uns überprüfen, ob unsere KlasseTeamden Verträgenequals() undhashCode()folgt:

@Test
public void equalsHashCodeContracts() {
    EqualsVerifier.forClass(Team.class).verify();
}

Es ist erwähnenswert, dassEqualsVerifier sowohl dieequals()- als auch diehashCode()-Methode testet.

EqualsVerifier is much stricter than the Java SE contract. Zum Beispiel wird sichergestellt, dass unsere Methoden keinNullPointerException. auslösen können. Außerdem wird erzwungen, dass beide Methoden oder die Klasse selbst endgültig sind.

Es ist wichtig zu erkennen, dassthe default configuration of EqualsVerifier allows only immutable fields. Dies ist eine strengere Überprüfung als es der Java SE-Vertrag zulässt. Dies entspricht einer Empfehlung von Domain-Driven Design, Wertobjekte unveränderlich zu machen.

Wenn wir einige der integrierten Einschränkungen für unnötig halten, können wir unserem Aufruf vonEqualsVerifier einsuppress(Warning.SPECIFIC_WARNING) hinzufügen.

7. Fazit

In diesem Artikel haben wir die Verträgeequals() undhashCode()besprochen. Wir sollten uns daran erinnern:

  • Überschreiben Sie immerhashCode(), wenn wirequals() überschreiben

  • Überschreiben Sieequals() undhashCode()  für Wertobjekte

  • Beachten Sie die Fallen beim Erweitern von Klassen, dieequals() undhashCode() überschrieben haben

  • Erwägen Sie die Verwendung einer IDE oder einer Bibliothek eines Drittanbieters zum Generieren der Methodenequals() undhashCode()

  • Erwägen Sie die Verwendung von EqualsVerifier, um unsere Implementierung zu testen

Schließlich können alle Codebeispieleover on GitHub gefunden werden.