Если два объекта равны по equals(), что можно сказать об их hashCode()?
Если два объекта равны по equals(), их hashCode() обязательно должен быть одинаковым. Это одно из главных правил Java.
🟢 Junior Level
Если два объекта равны по equals(), их hashCode() обязательно должен быть одинаковым. Это одно из главных правил Java.
Почему это важно? HashMap сначала ищет бакет по hashCode(), и только внутри бакета проверяет equals().
Зачем два уровня: hashCode быстрый (одна операция), но неточный (коллизии). equals точный (сравнение всех полей), но медленный. Сначала быстрый отсев — потом точная проверка. Если хеши разные — HashMap даже не вызовет equals() и не найдёт элемент.
Пример:
Person p1 = new Person("Ivan", 30);
Person p2 = new Person("Ivan", 30);
System.out.println(p1.equals(p2)); // true (если правильно реализован)
System.out.println(p1.hashCode() == p2.hashCode()); // ОБЯЗАН быть true!
Простое правило: Равные объекты = одинаковый hashCode. Всегда.
🟡 Middle Level
Почему это критически важно?
Поиск в HashMap работает так:
- Вычисляется
hashCode()ключа - Определяется бакет:
index = (n-1) & hash - Внутри бакета ищется элемент через
equals()
Если hashCode разный при равных equals:
- HashMap пойдёт в другой бакет
get()вернётnull, хотя элемент естьHashSetдопустит дубликатыremove()не удалит объект
Последствия нарушения
| Проблема | Описание |
|---|---|
| Потеря данных | get() возвращает null для существующего ключа |
| Дубликаты | HashSet содержит “одинаковые” объекты |
| Утечка памяти | Невозможно удалить объект из карты |
Реализация в JDK
- String: одинаковые строки → одинаковый hashCode (формула на основе символов)
- Integer: hashCode = само значение int
- Long: hashCode =
(int)(value ^ (value >>> 32)) - Object (по умолчанию): identity hash code — число, которое JVM связывает с объектом при первом вызове
System.identityHashCode(). Оно НЕ равно адресу в памяти (хотя может быть связано) и НЕ меняется при GC-перемещении.
Как проверить
Статические анализаторы (SonarQube, SpotBugs) автоматически находят нарушение этого контракта. IntelliJ IDEA предупреждает при генерации одного метода без другого.
Когда одинакового hashCode недостаточно
Одинаковый hashCode для равных объектов — необходимое, но недостаточное условие. hashCode должен быть консистентным: если поля не менялись, повторный вызов hashCode() должен вернуть то же число. Если hashCode зависит от текущего времени или рандома — контракт нарушен.
🔴 Senior Level
Формальное требование
Из Javadoc Object.hashCode():
If two objects are equal according to the
equals(Object)method, then calling thehashCodemethod on each of the two objects must produce the same integer result.
Это обязательство, а не рекомендация. Его нарушение ломает весь Java Collections Framework.
Internal Mechanics нарушения
При map.put(objA, "value"):
hashA = hash(objA.hashCode()) = 100 → бакет 100
При map.get(objB) где objB.equals(objA) == true:
hashB = hash(objB.hashCode()) = 200 → бакет 200 → пустой → null
Элемент физически в бакете 100, но HashMap ищет в бакете 200.
Heisenbug-эффект
Ошибка может не проявляться, если:
- Объекты не используются в хеш-коллекциях
- Разные hashCode случайно попали в один бакет (индекс совпал через
(n-1) & hash) - При следующем resize индексы изменятся, и код упадёт
Это создаёт недетерминированный баг, который сложно воспроизвести.
Memory and GC Impact
“Потерянные” элементы в HashMap продолжают занимать память, но недоступны для удаления. При массовом нарушении — утечка памяти в Old Gen.
Record и Lombok
// Record (Java 14+) — контракт соблюдён автоматически
public record Key(String id) {}
// Lombok — оба метода генерируются из одних полей
@EqualsAndHashCode
class Key { String id; }
Production Diagnostics
Если в production get() возвращает null для “существующего” ключа:
- Проверьте, переопределены ли оба метода
- Проверьте, не изменились ли mutable поля ключа
- Проверьте, не нарушена ли симметричность при наследовании
🎯 Шпаргалка для интервью
Обязательно знать:
- Если equals = true, то hashCode ОБЯЗАН быть одинаковым — обязательство из Javadoc
- HashMap ищет бакет по hashCode, внутри — по equals; разные hashCode = элемент не найден
- Нарушение = потеря данных в Map, дубликаты в Set, невозможность удаления
- Heisenbug-эффект: ошибка может не проявляться в малых тестах, но упасть в production
- Object.hashCode() по умолчанию = identity hash code, НЕ адрес в памяти
- Identity hash code не меняется при GC-перемещении
Частые уточняющие вопросы:
- Может ли hashCode совпасть случайно при разных equals? — да, это коллизия, нормальная ситуация
- Почему ошибка недетерминированная? — разные hashCode могут случайно попасть в один бакет при малой карте
- Что такое identity hash code? — число, которое JVM связывает с объектом при первом вызове System.identityHashCode()
- Как предотвратить? — использовать Records, Lombok @EqualsAndHashCode, IDE-генерацию
Красные флаги (НЕ говорить):
- «hashCode должен быть одинаковым только для равных объектов» — он МОЖЕТ совпадать и для неравных (коллизия)
- «Это редкая ошибка» — один из самых коварных багов: проходит тесты, падает в production
- «Можно игнорировать, если объекты не в Map» — да, но это хрупкое решение
Связанные темы:
- [[7. Что такое контракт equals() и hashCode()]]
- [[9. Если два объекта имеют одинаковый hashCode(), обязательно ли они равны по equals()]]
- [[10. Что произойдёт, если переопределить equals() но не переопределить hashCode()]]