Якщо два об'єкти рівні за 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()]]