Что такое контракт equals() и hashCode()?
Метод equals() должен вести себя как математическое понятие 'равенства':
🟢 Junior Level
Контракт equals() и hashCode() — это набор правил, которые должны соблюдать методы equals() и hashCode() в Java. Если их нарушить, коллекции вроде HashMap и HashSet будут работать неправильно.
Два простых правила:
- Если два объекта равны по
equals(), ихhashCode()обязан быть одинаковым - Если
hashCode()одинаковый — объекты не обязательно равны (это называется коллизия)
Пример правильного контракта:
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person p = (Person) o;
return age == p.age && name.equals(p.name);
}
@Override
public int hashCode() {
return 31 * name.hashCode() + age;
}
}
Главное правило: если объект будет использоваться как ключ в HashMap/HashSet — переопределяйте оба метода. Исключение: если вы НАМЕРЕННО хотите identity-поведение (например, для weak references).
🟡 Middle Level
Контракт equals()
Метод equals() должен вести себя как математическое понятие ‘равенства’:
- Рефлексивность: объект равен самому себе:
a.equals(a) = true - Симметричность: если A равен B, то B равен A
- Транзитивность: если A=B и B=C, то A=C
| Свойство | Описание |
|---|---|
| Рефлексивность | x.equals(x) всегда true |
| Симметричность | Если x.equals(y), то y.equals(x) |
| Транзитивность | Если x.equals(y) и y.equals(z), то x.equals(z) |
| Консистентность | Повторные вызовы дают один результат |
| Сравнение с null | x.equals(null) всегда false |
Контракт hashCode()
- Консистентность: значение не меняется, если не меняются поля из
equals() - Обязательная связь:
x.equals(y)→x.hashCode() == y.hashCode() - Необязательная связь: одинаковый hashCode ≠ равные объекты
Как правильно реализовывать
Алгоритм Джошуа Блоха:
int result = 17;
result = 31 * result + field1;
result = 31 * result + (field2 != null ? field2.hashCode() : 0);
return result;
Почему 17 и 31: 17 — начальное ‘зерно’ (prime seed), чтобы хеш первых полей не был нулевым. 31 — нечётное простое число: умножение на 31 = сдвиг влево на 5 минус 1 (31 * i = (i << 5) - i), что эффективно на CPU. Переполнение int — не баг, а норма: хеш ВСЕГДА wrap-around, это заложено в математику.
Типичные ошибки
- Переопределить equals, забыть hashCode — дубликаты в HashSet
- Использовать mutable поля — ключ “ломается” после изменения
- Классический баг JDK:
TimestampрасширяетDate.date.equals(timestamp)может вернуть true, ноtimestamp.equals(date)— false, потому что Timestamp проверяет наносекунды, которых нет у Date. Нарушена симметричность!
Когда НЕ использовать кастомные equals/hashCode
Не используйте equals/hashCode для объектов с business-идентичностью, которая может меняться. Например, entity с auto-generated ID: до сохранения id=null, после — id=42. Два объекта с id=null ‘равны’, но после сохранения — уже нет. Это нарушает консистентность в HashMap.
🔴 Senior Level
Internal Implementation в HashMap
HashMap использует оба метода для O(1) поиска:
hashCode()→ находит бакет:index = (n-1) & hash(key)equals()→ находит ключ внутри бакета
Последствия нарушения:
- Равные объекты с разными hashCode попадают в разные бакеты →
get()вернётnull HashSetдопустит дубликаты → нарушение бизнес-логикиremove()не найдёт объект → утечка памяти
Нарушение симметричности при наследовании
class Point2D { int x, y; }
class Point3D extends Point2D { int z; }
Если Point3D.equals() проверяет все 3 поля, а Point2D.equals() только 2:
new Point2D(1,1).equals(new Point3D(1,1,0))→true(Point2D не видит z)new Point3D(1,1,0).equals(new Point2D(1,1))→false(Point3D не instanceof Point3D)- Нарушение симметричности!
Решение: использовать getClass() вместо instanceof, или композицию вместо наследования.
Edge Cases
- URL.equals() — делает DNS-запрос, нарушает консистентность при сбое сети
- Float.NaN —
Float.NaN.equals(Float.NaN)= true, ноFloat.NaN == Float.NaN= false - Массивы —
equals()сравнивает ссылки, не содержимое; используйтеArrays.equals()
Performance Implications
- Плохой hashCode → коллизии → деградация HashMap до O(log n) или O(n)
- Тяжёлый equals() (сравнение больших строк/байтовых массивов) → большая константа в O(1)
- Кэширование hashCode (как в
String) — критично для производительности
Java 14+: Records
public record Person(String name, int age) {}
Records автоматически генерируют equals() и hashCode() на основе всех компонентов, гарантируя соблюдение контракта.
Security
Нарушение контракта может приводить к DoS-атам через Hash Flooding. Качественный hashCode() — это не только корректность, но и безопасность.
🎯 Шпаргалка для интервью
Обязательно знать:
- equals() — рефлексивность, симметричность, транзитивность, консистентность, null = false
- hashCode() — если equals = true, то hashCode ОБЯЗАН совпадать
- Обратное неверно: одинаковый hashCode ≠ равные объекты (коллизия)
- Алгоритм Блоха: начальное 17, множитель 31 (=(i«5)-i), включать все поля из equals
- 31 — простое нечётное, JVM оптимизирует умножение через сдвиг
- Переполнение int — норма, хеш всегда wrap-around
- Records (Java 14+) автоматически генерируют оба метода
Частые уточняющие вопросы:
- Что будет если переопределить только equals? — равные объекты попадут в разные бакеты, get вернёт null
- Почему 31, а не 17 или 37? — 31 = (i«5)-i, одна операция вместо умножения
- Что такое нарушение симметричности? — A.equals(B) = true, но B.equals(A) = false (классический баг: Timestamp vs Date)
- Когда НЕ переопределять? — если нужна identity-семантика (сравнение по ссылке, а не содержимому)
Красные флаги (НЕ говорить):
- «hashCode должен быть уникальным» — нет, коллизии допустимы
- «Можно переопределить только один метод» — нет, всегда пара
- «URL хороший ключ» — нет, URL.equals() делает DNS-запрос, нарушает консистентность
Связанные темы:
- [[3. Как HashMap определяет, в какой bucket положить элемент]]
- [[8. Если два объекта равны по equals(), что можно сказать об их hashCode()]]
- [[9. Если два объекта имеют одинаковый hashCode(), обязательно ли они равны по equals()]]
- [[10. Что произойдёт, если переопределить equals() но не переопределить hashCode()]]