Какие правила переопределения методов 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() должен:

  1. Возвращать одинаковое значение при каждом вызове на одном объекте, если его состояние не менялось;

  2. Если x.equals(y) — true, то x.hashCode() == y.hashCode();

  3. Необязательно: если 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?

Когда объект используется в качестве ключа:

  1. Сначала вызывается hashCode() → определяет корзину (bucket).

  2. Затем вызывается 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 обязательна
--- ---