Получить доступ

Равенство: ==, equals и hashCode

Идентичность против логического равенства, контракт equals и hashCode, проблема equals с наследованием и корректная реализация.

== против equals

Для примитивов == сравнивает значения. Для ссылочных типов == сравнивает идентичность — одна ли это ссылка на один объект в памяти, а equals() — логическое равенство по содержимому, ЕСЛИ метод переопределён.

По умолчанию Object.equals реализован как ==, то есть без переопределения «равенство по содержимому» не работает. Поэтому два разных объекта с одинаковыми полями равны по правильно написанному equals, но не по ==.

String a = new String("a");
String b = new String("a");
a == b;       // false — разные объекты в куче
a.equals(b);  // true  — одинаковое содержимое

Контракт equals

equals обязан быть: рефлексивным (x.equals(x) — true), симметричным (x.equals(y) ⇔ y.equals(x)), транзитивным (x=y и y=z ⇒ x=z), согласованным (повторные вызовы дают тот же результат, пока объекты не менялись) и возвращать false на null (x.equals(null) == false).

Эти правила не формальность: их нарушение делает поведение коллекций и сравнений непредсказуемым. Классический пример нарушения симметрии — equals, который пытается «понимать» чужой тип (например, регистронезависимая строка, считающая себя равной обычной String, тогда как String так не считает).

equals и наследование

Самая коварная тема: нельзя одновременно сохранить контракт equals и добавлять значимые поля в подкласс расширяемого класса. Если сравнивать через instanceof, симметрия/транзитивность ломаются между объектом базового и производного класса; если через getClass(), объекты разных классов никогда не равны — это нарушает принцип подстановки Лисков для подтипов.

Практические выходы: делать класс (или сам equals) final; либо предпочесть композицию наследованию — вынести «значимые поля» в отдельный объект, а не в подкласс. Записи (records) обходят проблему тем, что финальны и сравнивают строго по компонентам.

Контракт hashCode

Главное правило связки: если два объекта равны по equals, у них ОБЯЗАН совпадать hashCode. Обратное не требуется — неравные объекты могут иметь одинаковый хеш (коллизия — это нормально). В пределах одного запуска hashCode стабилен, пока значимые поля не меняются.

Нарушение этого правила тихо ломает HashMap/HashSet: объект кладётся в бакет по hashCode, а ищется по нему же — если хеши «разъехались» с равенством, объект уходит не в тот бакет и «теряется» (get вернёт null для логически того же ключа).

Как реализовать правильно

equals и hashCode должны опираться на ОДИН И ТОТ ЖЕ набор значимых полей. Удобные помощники: Objects.equals(a, b) (null-безопасное сравнение) в equals и Objects.hash(f1, f2, …) в hashCode (для горячих путей — ручная формула 31*h + field, чтобы не аллоцировать массив varargs).

В современном коде это чаще всего не пишут руками: records генерируют equals/hashCode/toString из компонентов автоматически, а для классов их генерирует IDE. Главное — держать наборы полей синхронными между двумя методами при любом изменении класса.

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

Изменяемые ключи

Если hashCode (или equals) зависит от изменяемого поля и это поле поменять ПОСЛЕ вставки объекта в HashMap/HashSet, объект становится недостижим: он лежит в старом бакете, а ищется по новому хешу.

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

Как отвечать на собеседовании

Различите == (идентичность/значение примитива) и equals (логическое равенство, по умолчанию == у Object), перечислите пять свойств контракта equals и главное правило hashCode (equal ⇒ равный hashCode) с последствием для HashMap. Покажите зрелость на теме equals+наследование (instanceof ломает симметрию, getClass ломает Лисков → final/композиция/records) и на синхронности полей equals/hashCode + неизменяемости ключей.

Вопросы по теме

Читайте также

Закрытая бета

Знать тему — половина дела

На живом собеседовании sotto слышит вопрос интервьюера и показывает короткий ответ и план. Оставьте контакт — пришлём доступ и промокод.

  • Промокод на 2 часа — сверх пробных 30 минут
  • Программа для Windows — скачать сразу, войти — через Яндекс ID или VK ID
  • Прямая связь с разработчиком: что улучшить — сделаем