W3docs

Java equals() и hashCode()

Правильно переопределяйте equals() и hashCode() в классах Java для работы с коллекциями и сравнения по значению.

equals и hashCode — два метода класса Object, на которые молча опираются коллекции на основе хэширования: HashMap, HashSet, LinkedHashMap и всё остальное, построенное на хэшировании. Реализуйте их правильно — и ваши объекты будут вести себя как значения: set.contains(point) найдёт точку независимо от того, какой экземпляр new Point(3, 4) вы передадите. Реализуйте неправильно — получите дубликаты в множествах, потерянные ключи в картах и ошибки, которые проявляются только под нагрузкой.

По умолчанию, унаследованный от Object, метод сравнивает идентичность: две ссылки равны только тогда, когда указывают на один и тот же объект. Это нормально для таких вещей, как соединения с базой данных, где каждый экземпляр является отдельным ресурсом. Для классов-значений — денег, точек, имён, дат — почти всегда нужно равенство по содержимому, а значит, нужно переопределять оба метода вместе.

Контракт

equals должен удовлетворять четырём правилам:

  • Рефлексивностьx.equals(x) возвращает true.
  • Симметричностьx.equals(y) тогда и только тогда, когда y.equals(x).
  • Транзитивность — если x.equals(y) и y.equals(z), то x.equals(z).
  • Согласованность — повторные вызовы с неизменёнными полями дают одинаковый результат.

Плюс: x.equals(null) должен возвращать false, но никогда не бросать исключение.

Для hashCode действует одно правило, связывающее его с equals:

  • Равные объекты должны иметь равные хэш-коды. Неравные объекты могут иметь одинаковый хэш-код (коллизии допустимы, но плохо влияют на производительность).

Именно это единственное правило объясняет, почему нельзя переопределить один метод без другого. Если a.equals(b), но a.hashCode() != b.hashCode(), то HashSet поместит их в разные корзины, contains найдёт не то, что нужно, — и у вас появится объект-призрак.

Смотрите, как нарушается контракт

Этот класс переопределяет equals, но забывает про hashCode, поэтому наследует хэш на основе идентичности из Object. Два объекта «равны», но попадают в разные корзины — contains не может найти только что добавленный объект:

java— editable, runs on the server

equals говорит, что объекты одинаковы, но множество не может найти второй из них. Переопределите hashCode в соответствии с equals — и поиск будет успешным.

Анатомия корректного equals

Рабочий equals следует стандартной схеме:

public final class Point {
  private final int x;
  private final int y;
  public Point(int x, int y) { this.x = x; this.y = y; }

  @Override
  public boolean equals(Object o) {
    if (this == o)                      return true;
    if (!(o instanceof Point p))        return false;
    return x == p.x && y == p.y;
  }

  @Override
  public int hashCode() {
    return Objects.hash(x, y);
  }
}

Шаг за шагом:

  • Быстрая проверка идентичности. this == o быстро обрабатывает частый случай.
  • Проверка типа с привязкой. instanceof Point p одним выражением отклоняет null и неподходящие типы, привязывая суженную ссылку.
  • Сравнение полей. Используйте == для примитивов, Objects.equals(a, b) для nullable-ссылок, Float.compare / Double.compare для чисел с плавающей точкой.

Objects.hash(...) строит хэш из списка полей. Это немного медленнее, чем написанный вручную код с XOR/умножением, но зато корректно и однозначно.

getClass или instanceof?

Два подхода:

  • instanceof позволяет экземпляру подкласса быть равным экземпляру родительского класса, если набор сравниваемых полей совпадает. Чуть более гибко.
  • getClass() требует точного совпадения типа времени выполнения. Легче обеспечить симметричность в иерархиях, но нарушает принцип подстановки.

Для большинства классов-значений простейший путь — объявить класс final и использовать instanceof. Без final смешивание двух стилей в иерархии — источник большинства ошибок равенства. Записи обходят эту проблему полностью: они неявно финальные, а сгенерированный equals использует точную проверку типа.

Поля с плавающей точкой

Не используйте == для полей double или float — значение +0.0 равно -0.0 при ==, но Double.compare различает их, а NaN == NaN — это false. Double.compare(a, b) == 0 и Float.compare дают согласованный ответ, которого требует контракт.

Массивы

Object.equals для массива сравнивает ссылки, а не содержимое. Используйте Arrays.equals(a, b) для одномерных массивов, Arrays.deepEquals — для многомерных. Аналогично используйте Arrays.hashCode / Arrays.deepHashCode в методе hashCode.

Изменяемость враждебна к коллекциям на основе хэширования

Если вы изменяете поле, участвующее в equals/hashCode, после того как объект уже помещён в HashSet, корзина, в которую множество его поместило, перестаёт соответствовать новому хэшу — и объект становится недостижимым через contains. Самое безопасное правило: поля, используемые в equals, должны быть final. Если это невозможно, никогда не помещайте объект в коллекцию на основе хэширования.

Не пишите эти методы вручную для простых классов данных

Если класс является чистым контейнером данных, предпочтите record — компилятор сгенерирует для вас корректные equals и hashCode, и они всегда будут синхронизированы при изменении полей. Если использовать записи нельзя, команда «Generate equals/hashCode» в вашей IDE — следующий лучший вариант.

Разобранный пример

java— editable, runs on the server

Что дальше

equals позволяет объектам сравнивать себя; toString позволяет им описывать себя. Следующая глава посвящена переопределению toString для получения вывода, действительно полезного в логах, сообщениях об ошибках и отладчиках. Продолжайте с метода toString в Java.

Практика

Практика
Почему переопределять `equals` без переопределения `hashCode` — это ошибка?
Почему переопределять `equals` без переопределения `hashCode` — это ошибка?
Was this page helpful?