← Назад к списку
ТехническаяJava и KotlinJunior

Какой контракт связывает equals и hashCode? Что будет, если переопределить только equals?

Короткий ответ

  • Равные по equals объекты обязаны иметь одинаковый hashCode
  • Обратное неверно: равный hashCode не означает равенство
  • equals должен быть рефлексивным, симметричным, транзитивным и согласованным
  • Переопределил equals — обязан переопределить hashCode
  • Иначе HashMap/HashSet не найдут «равный» объект в другой корзине
  • Поля в equals и hashCode должны совпадать и быть неизменяемыми

equals и hashCode переопределяются только парой, иначе хеш-коллекции работают некорректно.

Как сказать вслух

пример ответа

Контракт простой: если два объекта равны по equals, их hashCode обязан совпадать. Если переопределить только equals, то два логически равных объекта получат разные хеши от Object и попадут в разные корзины HashMap — contains и get просто не найдут элемент. Поэтому эти методы всегда переопределяют вместе и на одном наборе полей.

Подробный ответ

Основной ответ

Контракт из Javadoc: equals задаёт отношение эквивалентности (рефлексивность, симметричность, транзитивность, согласованность, неравенство null), а hashCode обязан возвращать одно и то же значение для равных объектов в рамках одного запуска. Разные объекты могут иметь одинаковый хеш — это коллизия, и это нормально. Хеш-коллекции сначала находят корзину по hashCode и только внутри неё сравнивают через equals. Если переопределён только equals, логически равные объекты окажутся в разных корзинах: HashSet допустит «дубликаты», HashMap.get вернёт null. Практичный способ не ошибаться — record, Lombok @EqualsAndHashCode или генерация IDE через Objects.equals/Objects.hash на одном наборе неизменяемых полей.

Ключевые моменты

  • Главное правило. equals равны ⇒ hashCode равны. Коллизии хешей допустимы, несогласованность — нет.
  • Механика поломки. HashMap ищет корзину по хешу; без hashCode равный ключ ищется не там, где лежит.
  • Выбор полей. Одинаковый набор полей в обоих методах; лучше неизменяемые бизнес-идентификаторы.
  • Инструменты. record генерирует оба метода автоматически; для JPA-сущностей обычно сравнивают по id с осторожностью.

Практический контекст

Вопрос-фильтр на джуниорских собеседованиях, но ошибки с ним встречаются и в проде: сущности в HashSet «дублируются», кэши по ключу-DTO промахиваются. Отдельная боль — JPA-сущности, где id появляется после сохранения, и ленивые прокси; про это любят спрашивать дальше. Интервьюер ждёт и формулировку контракта, и объяснение, как именно ломаются хеш-коллекции.

Пример кода

class Point {
    final int x, y;
    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 java.util.Objects.hash(x, y);
    }
}
// Или просто: record Point(int x, int y) {}

Частые ошибки

  • Утверждают, что одинаковый hashCode означает равенство объектов
  • Используют в equals сравнение через == для строк или оберток
  • Включают в hashCode изменяемые поля, из-за чего объект «теряется» в HashSet после мутации

ИП Кочкин Алексей Сергеевич · ИНН 390509026279 · ОГРНИП 325390000030973 · jiniys2005@yandex.ru