Java equals () i hashCode () Kontrakty

1. Przegląd

W tym samouczku przedstawimy dwie metody, które ściśle do siebie pasują : equals () i hashCode () . Skoncentrujemy się na ich wzajemnych relacjach, na tym, jak poprawnie je zastąpić i dlaczego powinniśmy zastąpić oba lub żadne.

2. równa się ()

Do obiektu klasa definiuje zarówno equals () i hashCode () metody - co oznacza, że te dwie metody są domyślnie zdefiniowane w każdej klasie Java, w tym te, które tworzą:

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

Spodziewalibyśmy się, że dochód. Równowartość (wydatki) zwróci prawdę . Ale z klasą Money w jej obecnej formie tak się nie stanie.

Domyślna implementacja equals () w klasie Object mówi, że równość jest tym samym, co tożsamość obiektu. A dochody i wydatki to dwa różne przypadki.

2.1. Zastąpienie równa się ()

Zastąpmy metodę equals () , aby nie brała pod uwagę tylko tożsamości obiektu, ale raczej wartości dwóch odpowiednich właściwości:

@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. równa się () Kontrakt

Java SE definiuje kontrakt, który musi spełniać nasza implementacja metody equals () . Większość kryteriów to zdrowy rozsądek. Metoda equals () musi być:

  • refleksyjny : przedmiot musi sobie równać
  • symetryczny : x. quals (y) musi zwracać ten sam wynik co y. quals (x)
  • przechodni : jeśli x. quals (y) i y. quals (z), to także x. quals (z)
  • spójny : wartość equals () powinna się zmieniać tylko wtedy, gdy właściwość zawarta w equals () zmienia się (nie jest dozwolona przypadkowość)

Dokładne kryteria możemy znaleźć w dokumentacji Java SE dla klasy Object .

2.3. Naruszenie równa się () Symetria z dziedziczeniem

Jeśli kryterium równości () jest takim zdrowym rozsądkiem, jak możemy go w ogóle naruszyć? Cóż, naruszenia zdarzają się najczęściej, jeśli rozszerzymy klasę, która nadpisała equals () . Rozważmy klasę Voucher, która rozszerza naszą klasę Money :

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

Na pierwszy rzut oka klasa Voucher i jej nadpisanie dla równości () wydają się być poprawne. Obie metody equals () zachowują się poprawnie, o ile porównujemy Pieniądze z pieniędzmi lub kupon z kuponem . Ale co się stanie, jeśli porównamy te dwa obiekty?

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.

To narusza kryteria symetrii kontraktu equals () .

2.4. Naprawianie równości () Symetria z kompozycją

Aby uniknąć tej pułapki, powinniśmy przedkładać kompozycję nad dziedziczenie.

Zamiast tworzyć podklasy Money , utwórzmy klasę Voucher z właściwością Money :

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 }

A teraz równość będzie działać symetrycznie, zgodnie z wymogami umowy.

3. hashCode ()

hashCode () zwraca liczbę całkowitą reprezentującą bieżące wystąpienie klasy. Należy obliczyć tę wartość zgodnie z definicją równości dla klasy. Dlatego jeśli przesłonimy metodę equals () , musimy również przesłonić hashCode () .

Aby uzyskać więcej informacji, zapoznaj się z naszym przewodnikiem po hashCode () .

3.1. hashCode () Kontrakt

Java SE definiuje również kontrakt dla metody hashCode () . Dokładne przyjrzenie się temu pokazuje, jak blisko powiązane są hashCode () i equals () .

Wszystkie trzy kryteria w kontrakcie funkcji hashCode () w pewnym sensie wspominają o metodzie equals () :

  • spójność wewnętrzna : wartość hashCode () może się zmienić tylko wtedy, gdy właściwość w equals () ulegnie zmianie
  • equals spójność : obiekty, które są sobie równe, muszą zwracać ten sam hashCode
  • kolizje : nierówne obiekty mogą mieć ten sam hashCode

3.2. Naruszenie spójności funkcji hashCode () i equals ()

Drugie kryterium kontraktu metod hashCode ma ważną konsekwencję: jeśli nadpisujemy equals (), musimy również przesłonić hashCode (). Jest to zdecydowanie najbardziej rozpowszechnione naruszenie dotyczące umów metod equals () i hashCode () .

Zobaczmy taki przykład:

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

Do zespołu przesłonięcia klasy tylko równa () , ale nadal niejawnie wykorzystuje implementację domyślnej hashCode () zdefiniowaną w Object klasy. A to zwraca inny hashCode () dla każdej instancji klasy. To narusza drugą zasadę.

Teraz, jeśli utworzymy dwa obiekty Team , oba z miastem „Nowy Jork” i działem „marketing”, będą one równe, ale zwrócą różne hashCodes.

3.3. Klucz HashMap z niespójnym hashCode ()

Ale dlaczego naruszenie kontraktu w naszej klasie Team jest problemem? Cóż, problem zaczyna się, gdy w grę wchodzą kolekcje oparte na skrótach. Spróbujmy użyć naszej klasy Team jako klucza do 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);

Spodziewalibyśmy się, że myTeamLeader zwróci „Anne”. Ale przy obecnym kodzie tak nie jest.

Jeśli chcemy używać instancji klasy Team jako kluczy HashMap , musimy przesłonić metodę hashCode () tak, aby była zgodna z kontraktem: Equal obiekty zwracają ten sam hashCode.

Zobaczmy przykładową implementację:

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

Po tej zmianie Leaders.get (myTeam) zwraca „Anne” zgodnie z oczekiwaniami.

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()
  • Zastąp equals () i hashCode () dla obiektów wartości
  • Uważaj na pułapki rozszerzania klas, które przesłoniły equals () i hashCode ()
  • Rozważ użycie IDE lub biblioteki innej firmy do generowania metod equals () i hashCode ()
  • Rozważ użycie EqualsVerifier, aby przetestować naszą implementację

Na koniec wszystkie przykłady kodu można znaleźć w serwisie GitHub.