Тема equals() и hashCode() уже настолько заезжена, что почти любой разработчик без запинки скажет:
"Если переопределил equals(), нужно переопределить и hashCode()."
Но на собеседованиях обычно валятся не на этом. Валятся на понимании почему. Например, что выведет этот код?
class Person {
private String name;
// equals() и hashCode() используют поле name
}
Person p = new Person("Andy");
Set<Person> set = new HashSet<>();
set.add(p);
p.setName("John");
System.out.println(set.contains(p));
Большинство отвечает:
true
Ведь мы ищем тот же самый объект. Но правильный ответ — false.
Почему?
Когда объект попадает в HashSet, сначала вычисляется его hashCode(). На основании этого значения определяется bucket, в который объект будет помещён. После того как мы изменили name, изменился и hashCode(). Теперь contains() вычисляет уже новый hash и ищет объект совсем в другом bucket.
Самое забавное, что объект никуда не делся — он всё ещё лежит внутри HashSet. Просто коллекция больше не может его найти.
Именно поэтому поля, участвующие в equals() и hashCode(), должны быть неизменяемыми. Или как минимум не должны изменяться после помещения объекта в HashMap или HashSet.
Есть ещё одна классическая ловушка. Некоторые переопределяют только equals():
@Override
public boolean equals(Object o) {
...
}
и забывают про hashCode().
В результате:
Person p1 = new Person("Andy");
Person p2 = new Person("Andy");
System.out.println(p1.equals(p2)); // true
Set<Person> set = new HashSet<>();
set.add(p1);
System.out.println(set.contains(p2)); // false
Почему?
Потому что HashSet сначала ищет объект по hashCode(). Если hash отличается, до вызова equals() дело вообще может не дойти.
Отсюда и контракт:
- если equals() возвращает true, то hashCode() обязан быть одинаковым;
- одинаковый hashCode() не означает, что объекты равны — в одном bucket могут лежать разные объекты, и тогда уже вызывается equals().
Именно это, кстати, часто забывают. Многие думают, что hashCode() должен быть уникальным.
Нет.
Он нужен не для определения равенства, а для быстрого поиска нужного bucket. Финальное решение о равенстве почти всегда принимает именно equals().
Вот это уже тот момент, который действительно важно понимать.
Потому что вопрос на собеседовании обычно звучит не «что такое equals() и hashCode()», а «почему HashSet перестал находить объект, который в нём лежит?».