Java entspricht () und hashCode () Verträgen

1. Übersicht

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

2. gleich ()

Die Object- Klasse definiert sowohl die Methoden equals () als auch hashCode (). 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 , income.equals (Aufwendungen) zurückzukehren wahr . Aber mit der Money- Klasse in ihrer aktuellen Form wird dies nicht der Fall sein.

Die Standardimplementierung von equals () in der Klasse Object besagt, dass Gleichheit mit Objektidentität identisch ist. Und Einnahmen und Ausgaben sind zwei verschiedene Beispiele.

2.1. Überschreiben gleich ()

Überschreiben wir die Methode equals () , 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) 

2.2. gleich () Vertrag

Java SE definiert einen Vertrag, den unsere Implementierung der equals () -Methode erfüllen muss. Die meisten Kriterien sind gesunder Menschenverstand. Die Methode equals () muss sein:

  • reflexiv : Ein Objekt muss sich selbst gleichstellen
  • symmetrisch : x.equals (y) muss das gleiche Ergebnis wie y.equals (x) zurückgeben.
  • transitiv : wenn x.equals (y) und y.equals (z), dann auch x.equals (z)
  • konsistent : Der Wert von equals () sollte sich nur ändern, wenn sich eine in equals () enthaltene Eigenschaft ändert (keine Zufälligkeit zulässig).

Wir können die genauen Kriterien in den Java SE-Dokumenten für die Object- Klasse nachschlagen .

2.3. Verletzung gleich () Symmetrie mit Vererbung

Wenn das Kriterium für equals () so vernünftig ist, wie können wir es überhaupt verletzen? Nun, Verstöße treten am häufigsten auf, wenn wir eine Klasse erweitern, die equals () überschrieben hat . Betrachten wir einen Gutschein - Klasse , die unsere erstreckt Geld Klasse:

class WrongVoucher extends Money { private String store; @Override public boolean equals(Object o)  // other methods }

Auf den ersten Blick scheinen die Voucher- Klasse und ihre Überschreibung für equals () korrekt zu sein. Und beide equals () -Methoden verhalten sich korrekt, solange wir Geld mit Geld oder Gutschein mit Gutschein vergleichen . Aber was passiert, wenn wir diese beiden Objekte vergleichen?

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 des equals () - Vertrags.

2.4. Fixing equals () Symmetrie mit Komposition

Um diese Gefahr zu vermeiden, sollten wir die Komposition der Vererbung vorziehen .

Anstatt Money zu unterordnen, erstellen wir eine Voucher- Klasse mit einer Money- 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)  // other methods }

Und jetzt arbeiten Gleichgestellte 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. So , wenn wir die überschreiben equals () Methode, müssen wir auch überschreiben hashCode () .

Weitere Informationen finden Sie in unserer Anleitung zu hashCode () .

3.1. hashCode () Vertrag

Java SE definiert auch einen Vertrag für die hashCode () -Methode. Ein genauer Blick darauf zeigt, wie eng hashCode () und equals () verwandt sind.

Alle drei Kriterien im Vertrag von hashCode () erwähnen in gewisser Weise die equals () -Methode:

  • Interne Konsistenz : Der Wert von hashCode () kann sich nur ändern, wenn sich eine Eigenschaft in equals () ändert
  • Gleich Konsistenz : Objekte, die einander gleich sind, müssen denselben Hashcode zurückgeben
  • Kollisionen : Ungleiche Objekte haben möglicherweise denselben Hashcode

3.2. Verletzung der Konsistenz von hashCode () und equals ()

Das zweite Kriterium des Vertrags mit den hashCode-Methoden hat eine wichtige Konsequenz: Wenn wir equals () überschreiben, müssen wir auch hashCode () überschreiben. Dies ist bei weitem der am weitesten verbreitete Verstoß gegen die Verträge der Methoden equals () und hashCode () .

Sehen wir uns ein solches Beispiel an:

class Team { String city; String department; @Override public final boolean equals(Object o) { // implementation } }

Die Team- Klasse überschreibt nur equals () , verwendet jedoch implizit die in der Object- Klasse definierte Standardimplementierung von hashCode () . Und dies gibt für jede Instanz der Klasse einen anderen hashCode () zurück . Dies verstößt gegen die zweite Regel.

Nun , wenn wir zwei erstellen Team - Objekte, die beide mit Stadt „New York“ und Abteilung „Marketing“, werden sie gleich sein, aber sie werden anders Hashcodes zurück.

3.3. HashMap- Schlüssel mit inkonsistentem hashCode ()

But why is the contract violation in our Team class a problem? Well, the trouble starts when some hash-based collections are involved. Let's try to use our Team class as a key of a HashMap:

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);

We would expect myTeamLeader to return “Anne”. But with the current code, it doesn't.

If we want to use instances of the Team class as HashMap keys, we have to override the hashCode() method so that it adheres to the contract: Equal objects return the same hashCode.

Let's see an example implementation:

@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; }

After this change, leaders.get(myTeam) returns “Anne” as expected.

4. When Do We Override equals() and hashCode()?

Generally, we want to override either both of them or neither of them. We've just seen in Section 3 the undesired consequences if we ignore this rule.

Domain-Driven Design can help us decide circumstances when we should leave them be. For entity classes – for objects having an intrinsic identity – the default implementation often makes sense.

However, for value objects, we usually prefer equality based on their properties. Thus want to override equals() and hashCode(). Remember our Money class from Section 2: 55 USD equals 55 USD – even if they're two separate instances.

5. Implementation Helpers

We typically don't write the implementation of these methods by hand. As can be seen, there are quite a few pitfalls.

One common way is to let our IDE generate the equals() and hashCode() methods.

Apache Commons Lang and Google Guava have helper classes in order to simplify writing both methods.

Project Lombok also provides an @EqualsAndHashCode annotation. Note again how equals() and hashCode() “go together” and even have a common annotation.

6. Verifying the Contracts

If we want to check whether our implementations adhere to the Java SE contracts and also to some best practices, we can use the EqualsVerifier library.

Let's add the EqualsVerifier Maven test dependency:

 nl.jqno.equalsverifier equalsverifier 3.0.3 test 

Let's verify that our Team class follows the equals() and hashCode() contracts:

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

It's worth noting that EqualsVerifier tests both the equals() and hashCode() methods.

EqualsVerifier is much stricter than the Java SE contract. For example, it makes sure that our methods can't throw a NullPointerException. Also, it enforces that both methods, or the class itself, is final.

It's important to realize that the default configuration of EqualsVerifier allows only immutable fields. This is a stricter check than what the Java SE contract allows. This adheres to a recommendation of Domain-Driven Design to make value objects immutable.

If we find some of the built-in constraints unnecessary, we can add a suppress(Warning.SPECIFIC_WARNING) to our EqualsVerifier call.

7. Conclusion

In this article, we've discussed the equals() and hashCode() contracts. We should remember to:

  • Always override hashCode() if we override equals()
  • Überschreiben Sie equals () und hashCode () für Wertobjekte
  • Beachten Sie die Fallen beim Erweitern von Klassen, die equals () und hashCode () überschrieben haben.
  • Erwägen Sie die Verwendung einer IDE oder einer Bibliothek eines Drittanbieters zum Generieren der Methoden equals () und hashCode ()
  • Erwägen Sie die Verwendung von EqualsVerifier, um unsere Implementierung zu testen

Schließlich finden Sie alle Codebeispiele auf GitHub.