Контракты equals () и hashCode () в Java

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)

Мы ожидаем, что yield.equals (расходы) вернут true . Но с классом 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. equals () Контракт

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 () с наследованием

Если критерий equals () - это такой здравый смысл, как мы можем вообще его нарушить? Что ж, нарушения случаются чаще всего, если мы расширяем класс, который переопределил equals () . Давайте рассмотрим класс Voucher, который расширяет наш класс Money :

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

На первый взгляд, класс Voucher и его переопределение для 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. Исправление симметрии equals () с помощью композиции

Чтобы избежать этой ловушки, мы должны отдавать предпочтение композиции, а не наследованию.

Вместо создания подкласса Money давайте создадим класс Voucher со свойством 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 () для каждого экземпляра класса. Это нарушает второе правило.

Теперь, если мы создадим два объекта Team , оба с городом «Нью-Йорк» и отделом «маркетинг», они будут равны, но будут возвращать разные хэш-коды.

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 () для объектов значений
  • Помните о ловушках расширения классов, которые переопределяют equals () и hashCode ()
  • Рассмотрите возможность использования IDE или сторонней библиотеки для создания методов equals () и hashCode ()
  • Рассмотрите возможность использования EqualsVerifier для тестирования нашей реализации

Наконец, все примеры кода можно найти на GitHub.