Какой контракт связывает 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 после мутации