Контракт hashCode() и equals()
«Переопределили equals(), но забыли hashCode() — что сломается?» Это вопрос, который отделяет тех, кто просто пользуется HashMap, от тех, кто понимает, как она устроена внутри.
Контракт hashCode() и equals() — это правило, которое обязаны соблюдать вместе оба метода: если два объекта равны по
equals(), у них ОБЯЗАН быть одинаковыйhashCode(). Нарушишь — HashMap и HashSet начнут вести себя непредсказуемо.
Вопрос-крючок
class Point {
int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Point)) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
// hashCode() не переопределён!
}
Set<Point> visited = new HashSet<>();
visited.add(new Point(1, 1));
System.out.println(visited.contains(new Point(1, 1))); // ?
Вывод:
falseЛогически кажется, что должно быть true — ведь equals() явно говорит, что точки с одинаковыми x и y равны. Но HashSet не вызывает equals() у всех подряд элементов. Сначала он вычисляет hashCode() нового объекта, чтобы понять, в какую «корзину» (bucket) заглянуть, и сравнивает объекты только внутри этой же корзины. Раз hashCode() не переопределён, используется версия из Object по умолчанию — она обычно основана на адресе объекта в памяти и у двух разных объектов Point(1, 1) почти наверняка окажется разной. Значит, новый Point(1, 1) попадёт в другую корзину, где старого объекта просто нет — и contains() вернёт false, даже несмотря на то, что equals() для этих объектов дал бы true.
Как HashMap использует хеш и equals вместе
Внутри HashMap (и HashSet, который на HashMap и построен) хранится массив «корзин». Когда ты добавляешь элемент, Java сначала вызывает его hashCode() и по этому числу быстро вычисляет номер корзины — это позволяет находить нужный элемент почти мгновенно, а не перебирать все элементы подряд. Но разные объекты иногда могут дать одинаковый хеш (это называется коллизией) — тогда внутри одной корзины может лежать несколько элементов, и вот тут уже вступает в дело equals(): чтобы понять, какой из нескольких элементов в корзине — тот самый, Java сравнивает их через equals() один за другим.
То есть hashCode() — это «быстрый ориентир, куда примерно смотреть», а equals() — «точная проверка, тот ли это объект». Оба метода обязаны работать в связке, поэтому и существует контракт между ними.
Формулировка контракта
Контракт, описанный в документации класса Object, звучит так:
- Если два объекта равны согласно
equals(), у них обязательно должен быть одинаковыйhashCode(). - Обратное необязательно: у объектов с одинаковым
hashCode()equals()вполне может вернутьfalse(это и есть коллизия — нормальная ситуация). hashCode()должен возвращать одно и то же число при повторных вызовах для одного и того же объекта (если его поля не менялись).
Если переопределяешь equals() — переопределяй и hashCode() так, чтобы он использовал те же поля, что и сравнение в equals(). Для класса Point из примера правильный hashCode() выглядит так:
@Override
public int hashCode() {
return Objects.hash(x, y);
}
Метод Objects.hash() — стандартный помощник из java.util.Objects, который аккуратно комбинирует хеши нескольких полей в одно число. После добавления такого hashCode() пример из начала урока начнёт возвращать true — потому что оба объекта Point(1, 1) теперь попадают в одну и ту же корзину и сравниваются через корректный equals().
Частые ошибки на собеседовании
Переопределяют только equals(), забывая hashCode(). Код компилируется без единого предупреждения, тесты «в лоб» через equals() тоже проходят — но объект молча ломается внутри HashMap/HashSet, и баг всплывает только там.
Используют разные поля в equals() и hashCode() — например, equals() сравнивает только id, а hashCode() учитывает ещё и name. Формально оба метода «работают», но не согласованы друг с другом, и контракт нарушается.
Не понимают, зачем вообще нужен hashCode(), если есть equals() — мол, можно же просто сравнить все элементы по очереди. Можно, но тогда поиск в коллекции из миллиона элементов займёт миллион сравнений вместо одного быстрого вычисления хеша.
Итоги-шпаргалка
- Равные объекты (по
equals()) обязаны иметь одинаковыйhashCode()— это и есть контракт. HashMap/HashSetсначала используютhashCode(), чтобы найти нужную «корзину», и только потомequals(), чтобы сравнить объекты внутри неё.- Переопределил
equals()— обязательно переопредели иhashCode(), используя те же поля. - Нарушение контракта не даёт ошибку компиляции — объект просто «теряется» в хеш-коллекциях, и это трудно отладить.
Objects.hash(поле1, поле2, ...)— стандартный и надёжный способ написать корректныйhashCode().