Java дорівнює () і hashCode () контрактам

1. Огляд

У цьому підручнику ми представимо два методи, які тісно належать один одному : equals () та hashCode () . Ми зосередимося на їх взаєминах один з одним, на тому, як правильно їх замінити, і чому ми повинні замінити обидва або жодне.

2. дорівнює ()

Клас Object визначає як методи equals (), так і hashCode () - це означає, що ці два методи неявно визначаються в кожному класі Java, включаючи створені нами:

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

Ми очікуємо, що дохід.equals (витрати) повернуться істинними . Але з класом Money у його поточному вигляді це не буде.

За замовчуванням реалізація equals () у класі Object говорить, що рівність - це те саме, що ідентичність об’єкта. А доходи та витрати - це два різні випадки.

2.1. Заміщення дорівнює ()

Давайте перевизначимо метод equals (), щоб він не враховував лише ідентичність об’єкта, а також значення двох відповідних властивостей:

@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. дорівнює () Контракту

Java SE визначає контракт, який повинна виконувати наша реалізація методу equals () . Більшість критеріїв - це здоровий глузд. Метод equals () повинен бути:

  • рефлексивний : об’єкт повинен дорівнювати собі
  • симетричний : x.equals (y) повинен повертати той самий результат, що і y.equals (x)
  • перехідні : якщо x.equals (y) та y.equals (z), то також x.equals (z)
  • узгоджено : значення equals () має змінюватися лише за умови зміни властивості, що міститься у equals ( )

Точні критерії ми можемо знайти в Документах Java SE для класу Object .

2.3. Порушення рівної () симетрії зі спадкуванням

Якщо критерієм equals () є такий здоровий глузд, як ми взагалі можемо його порушити? Ну, порушення трапляються найчастіше, якщо ми розширимо клас, який перевизначив дорівнює () . Давайте розглянемо клас ваучера, який розширює наш клас Гроші :

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

На перший погляд, клас ваучера та його перевизначення для equals () здаються правильними. І обидва методи equals () поводяться правильно, якщо ми порівнюємо Гроші з Грошима або Ваучер з Ваучером . Але що станеться, якщо порівняти ці два об’єкти?

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.

Це порушує критерії симетрії контракту equals () .

2.4. Фіксація дорівнює () симетрії з композицією

Щоб уникнути цієї помилки, ми повинні віддавати перевагу композиції над спадщиною.

Замість того, щоб підкласувати Гроші , давайте створимо клас Ваучера із властивістю 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 }

І тепер рівноправні будуть працювати симетрично, як того вимагає контракт.

3. hashCode ()

hashCode () повертає ціле число, що представляє поточний екземпляр класу. Ми повинні обчислити це значення відповідно до визначення рівності для класу. Таким чином, якщо ми замінюємо метод equals () , ми також повинні замінити hashCode () .

Щоб отримати докладнішу інформацію, перегляньте наш посібник з hashCode () .

3.1. Контракт hashCode ()

Java SE також визначає контракт для методу hashCode () . Ретельний погляд на нього показує, наскільки тісно пов’язані між собою hashCode () і equals () .

Всі три критерії в контракті hashCode () дещо згадують метод equals () :

  • Внутрішня узгодженість : значення хеш - код () може змінитися тільки тоді , коли властивість , яке знаходиться в рівних () змінюється
  • дорівнює узгодженості : об’єкти, які рівні між собою, повинні повертати однаковий хеш-код
  • зіткнення : нерівні об'єкти можуть мати однаковий хеш-код

3.2. Порушення узгодженості hashCode () і equals ()

Другий критерій контракту методів hashCode має важливий наслідок: якщо ми перевизначимо equals (), ми також повинні перевизначити hashCode (). І це, безумовно, найбільш розповсюджене порушення щодо контрактів методів equals () та hashCode () .

Побачимо такий приклад:

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

Клас Team замінює лише equals () , але він все ще неявно використовує реалізацію hashCode () за замовчуванням, як визначено в класі Object . І це повертає інший hashCode () для кожного екземпляра класу. Це порушує друге правило.

Тепер, якщо ми створимо два об’єкти Команди , як з міським “Нью-Йорком”, так і з відділом “Маркетинг”, вони будуть рівні, але вони повернуть різні хеш-коди.

3.3. Клавіша HashMap із суперечливим 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()
  • Замінити equals () і hashCode () для об'єктів значення
  • Майте на увазі пастки розширення класів, які перевизначили дорівнює () та hashCode ()
  • Подумайте про використання IDE або сторонніх бібліотек для генерації методів equals () та hashCode ()
  • Подумайте про використання EqualsVerifier для перевірки нашої реалізації

Нарешті, усі приклади коду можна знайти на GitHub.