Рубрики
Без рубрики

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

Узнайте о контрактах, которые необходимо выполнить для equals() и hashcode (), и о взаимосвязи между этими двумя методами

Автор оригинала: baeldung.

1. Обзор

В этом уроке мы представим два метода, которые тесно связаны друг с другом: equals() и Хэш-код() . Мы сосредоточимся на их взаимоотношениях друг с другом, на том, как правильно переопределить их и почему мы должны переопределять оба или ни то, ни другое.

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)

Мы ожидаем, что доход.равен(расходам) вернется верно . Но с классом Деньги в его нынешнем виде этого не произойдет.

Реализация по умолчанию равно() в классе Объект говорит, что равенство – это то же самое, что идентичность объекта. И доход и расходы являются двумя различными примерами.

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)
      || (this.currencyCode != null && this.currencyCode.equals(other.currencyCode));
    return this.amount == other.amount && currencyCodeEquals;
}

2.2. равный() Контракт

Java SE определяет контракт, который должна выполнять наша реализация метода equals () . Большинство критериев-это здравый смысл. Метод equals() должен быть:

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

Мы можем найти точные критерии в документах Java SE для Объекта класса .

2.3. Нарушение Симметрии равенства() С Наследованием

Если критерий для равен() является таким здравым смыслом, как мы вообще можем его нарушать? Ну, нарушения происходят чаще всего, если мы расширяем класс, который переопределил equals() . Давайте рассмотрим Ваучер класс, который расширяет наш Денежный класс:

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
}

На первый взгляд, класс Ваучер и его переопределение для 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.

Это нарушает критерии симметрии равно() контракт.

2.4. Фиксация равенства() Симметрии С Композицией

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

Вместо подкласса Деньги давайте создадим Ваучер класс с Деньгами свойством:

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
}

И теперь равно будет работать симметрично, как того требует контракт.

3. Хэш-код()

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

Для получения более подробной информации ознакомьтесь с нашим руководством по hashCode() .

3.1. Контракт с хэш-кодом()

Java SE также определяет контракт для метода hashCode () . Тщательный взгляд на него показывает, насколько тесно связаны Хэш-код() и равно () .

Все три критерия в договоре о Хэш-код() упомяните в некоторых отношениях равно() метод:

  • внутренняя согласованность : значение Хэш-кода() может измениться только в том случае, если свойство, находящееся в равно () , изменяется
  • равно согласованности : объекты, которые равны друг другу, должны возвращать один и тот же хэш-код
  • столкновения : неодинаковые объекты могут иметь один и тот же хэш-код

3.2. Нарушение согласованности хэш-кода() и равно()

2-й критерий контракта методов хэш-кода имеет важное следствие: Если мы переопределяем equals(), мы также должны переопределить хэш-код(). И это, безусловно, самое распространенное нарушение в отношении контрактов методов equals() и hashCode () .

Давайте рассмотрим такой пример:

class Team {

    String city;
    String department;

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

Класс Team переопределяет только equals() , но он по-прежнему неявно использует реализацию hashCode() по умолчанию, как определено в классе Object . И это возвращает другой Хэш-код() для каждого экземпляра класса. Это нарушает второе правило.

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

3.3. Ключ хэш-карты С Несогласованным Хэш-кодом()

Но почему нарушение контракта в нашей команде классе является проблемой? Что ж, проблемы начинаются, когда задействованы некоторые коллекции на основе хэша. Давайте попробуем использовать наш Team класс в качестве ключа Хэш-карты :

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

Мы бы ожидали, что мой руководитель команды вернет “Энн”. Но с текущим кодом это не так.

Если мы хотим использовать экземпляры класса Team в качестве ключей HashMap , мы должны переопределить метод hashCode () , чтобы он соответствовал контракту: Равные объекты возвращают один и тот же Хэш-код.

Давайте рассмотрим пример реализации:

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

После этого изменения leaders.get(MyTeam) возвращает “Энн”, как и ожидалось.

4. Когда мы переопределяем equals() и hashCode()?

Как правило, мы хотим переопределить либо оба из них, либо ни один из них. Мы только что видели в разделе 3 нежелательные последствия, если мы проигнорируем это правило.

Дизайн, основанный на домене, может помочь нам решить, в каких обстоятельствах нам следует оставить их в покое. Для классов сущностей – для объектов, имеющих внутреннюю идентичность, – реализация по умолчанию часто имеет смысл.

Однако для объектов со значением мы обычно предпочитаем равенство, основанное на их свойствах . Таким образом, вы хотите переопределить equals() и Хэш-код() . Помните, что наш класс Деньги из раздела 2: 55 долларов США равняется 55 долларам США, даже если это два отдельных экземпляра.

5. Помощники По Внедрению

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

Один из распространенных способов – позволить вашей среде IDE генерировать методы equals() и hashCode () .

Apache Commons Lang и Google Guava имеют вспомогательные классы, чтобы упростить написание обоих методов.

Проект Ломбок также предоставляет аннотацию @EqualsAndHashCode . Еще раз обратите внимание, как равно() и Хэш-код() “идут вместе” и даже имеют общую аннотацию.

6. Проверка контрактов

Если мы хотим проверить, соответствуют ли наши реализации контрактам Java SE, а также некоторым рекомендациям, мы можем использовать библиотеку EqualsVerifier.

Давайте добавим зависимость EqualsVerifier Maven test:


    nl.jqno.equalsverifier
    equalsverifier
    3.0.3
    test

Давайте проверим, что наш Команда класс следует равным() и Хэш-коду() контрактам:

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

Стоит отметить, что EqualsVerifier проверяет методы equals() и hashCode () .

EqualsVerifier намного строже, чем контракт Java SE. Например, это гарантирует, что наши методы не смогут создать исключение NullPointerException. Кроме того, это гарантирует, что оба метода или сам класс являются окончательными.

Важно понимать, что конфигурация по умолчанию EqualsVerifier допускает только неизменяемые поля . Это более строгая проверка, чем то, что позволяет контракт Java SE. Это соответствует рекомендации доменного проектирования, чтобы сделать объекты значений неизменяемыми.

Если мы сочтем некоторые встроенные ограничения ненужными, мы можем добавить подавить(Предупреждение.SPECIFIC_WARNING) на наш Эквалайзер вызов.

7. Заключение

В этой статье мы обсудили контракты equals() и hashCode () . Мы должны помнить, что:

  • Всегда переопределяйте Хэш-код() если мы переопределяем равно()
  • Переопределение равно() и Хэш-код() для объектов значений
  • Имейте в виду ловушки расширения классов, которые переопределили equals() и Хэш-код()
  • Рассмотрите возможность использования IDE или сторонней библиотеки для создания методов equals() и хэш-кода()
  • Рассмотрите возможность использования EqualsVerifier для проверки нашей реализации

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