Какие правила переопределения методов hashCode и equals
Переопределение методов equals() и hashCode() в Java — ключевой момент при работе с коллекциями (например, HashSet, HashMap, Hashtable) и при сравнении объектов. Неправильная реализация может привести к неожиданным багам, когда объекты ведут себя некорректно в хеш-таблицах.
🔹 Что такое equals() и hashCode()?
Метод | Назначение |
---|---|
equals() | Определяет, равны ли два объекта по содержимому (логическому смыслу) |
--- | --- |
hashCode() | Возвращает целое число (хеш-код), используемое в хеш-таблицах (HashMap) |
--- | --- |
✅ Основные правила переопределения
🔁 1. Связь между equals() и hashCode()
Если два объекта равны по equals(), то их hashCode() ДОЛЖЕН быть одинаковым.
📌 Но обратное не обязательно: если хеши равны, объекты могут быть разными по содержанию (equals() может вернуть false).
⚠️ 2. Контракт метода equals()
Метод equals() должен удовлетворять пяти свойствам:
Свойство | Описание |
---|---|
Рефлексивность | x.equals(x) должно быть true |
--- | --- |
Симметричность | x.equals(y) ⇔ y.equals(x) |
--- | --- |
Транзитивность | Если x.equals(y) и y.equals(z), то x.equals(z) |
--- | --- |
Согласованность | Результат equals() должен оставаться одинаковым при повторных вызовах, если объекты не изменялись |
--- | --- |
Сравнение с null | x.equals(null) должно возвращать false |
--- | --- |
📏 3. Контракт метода hashCode()
Метод hashCode() должен:
-
Возвращать одинаковое значение при каждом вызове на одном объекте, если его состояние не менялось;
-
Если x.equals(y) — true, то x.hashCode() == y.hashCode();
-
Необязательно: если x.equals(y) — false, то hashCode() может быть одинаковым (допустимы коллизии, но нежелательны).
🛠 Пример правильной реализации
▶ Класс без переопределения:
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
}
Person p1 = new Person("Anna", 20);
Person p2 = new Person("Anna", 20);
System.out.println(p1.equals(p2)); // false ❌ — сравниваются ссылки
▶ Переопределение equals() и hashCode():
class Person {
String name;
int age;
Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. сравнение ссылок
if (o == null || getClass() != o.getClass()) return false; // 2. проверка типа
Person person = (Person) o;
return age == person.age && name.equals(person.name); // 3. сравнение содержимого
}
@Override
public int hashCode() {
return Objects.hash(name, age); // из java.util.Objects
}
}
Теперь:
Person p1 = new Person("Anna", 20);
Person p2 = new Person("Anna", 20);
System.out.println(p1.equals(p2)); // ✅ true
System.out.println(p1.hashCode() == p2.hashCode()); // ✅ true
🔍 Как работает hashCode() в HashMap?
Когда объект используется в качестве ключа:
-
Сначала вызывается hashCode() → определяет корзину (bucket).
-
Затем вызывается equals() для сравнения с другими элементами в этой корзине.
Если hashCode() плохой (одинаковый для многих объектов), все элементы попадают в одну корзину → теряется производительность.
❌ Ошибки, которых стоит избегать
Ошибка | Пример |
---|---|
Переопределён equals(), но не hashCode() | В HashMap одинаковые ключи попадут в разные корзины |
--- | --- |
Нарушен контракт equals() | Возвращает true при сравнении с null, нет симметрии и т.д. |
--- | --- |
Использование изменяемых объектов как ключей | Если поля объекта меняются после вставки в HashMap, он "теряется" |
--- | --- |
🧠 Полезные инструменты
-
Objects.equals(a, b) — безопасно сравнивает, даже если a или b равен null.
-
Objects.hash(a, b, c) — создаёт корректный хеш-код.
📌 Итоговая памятка:
Что нужно сделать | Пример |
---|---|
Переопределить equals() | Сравнивать содержимое объектов |
--- | --- |
Переопределить hashCode() | Использовать те же поля, что и в equals() |
--- | --- |
Соблюдать контракт equals() | Рефлексивность, симметричность, транзитивность |
--- | --- |
Не забывать о null | Проверка obj == null обязательна |
--- | --- |