Питання 8 · Розділ 10

Якщо два об'єкти рівні за equals(), що можна сказати про їх hashCode()?

Якщо два об'єкти рівні за equals(), їх hashCode() обов'язково повинен бути однаковим. Це одне з головних правил Java.

Мовні версії: English Russian Ukrainian

🟢 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 працює так:

  1. Обчислюється hashCode() ключа
  2. Визначається бакет: index = (n-1) & hash
  3. Всередині бакета шукається елемент через 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 the hashCode method 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 для “наявного” ключа:

  1. Перевірте, чи перевизначені обидва методи
  2. Перевірте, чи не змінилися mutable поля ключа
  3. Перевірте, чи не порушена симетричність при наслідуванні

🎯 Шпаргалка для співбесіди

Обов’язково знати:

  • Якщо 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()]]