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

Що станеться, якщо перевизначити hashCode(), але не перевизначити equals()?

Якщо перевизначити тільки hashCode(), об'єкти з однаковим хешем потраплять в одну корзину, але HashMap все одно вважатиме їх різними — тому що equals() за замовчуванням порівнює...

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

🟢 Junior Level

Якщо перевизначити тільки hashCode(), об’єкти з однаковим хешем потраплять в одну корзину, але HashMap все одно вважатиме їх різними — тому що equals() за замовчуванням порівнює посилання на об’єкти (==).

Що станеться:

public class User {
    private Long id;

    @Override
    public int hashCode() { return id != null ? id.intValue() : 0; }
    // equals() НЕ перевизначено — використовується Object.equals() (порівняння за посиланням)
}

Map<User, String> map = new HashMap<>();
map.put(new User(1L), "Alice");  // hashCode=1, бакет #1 → equals не викликано (бакет порожній) → OK
map.put(new User(1L), "Bob");    // hashCode=1, бакет #1 → equals(Alice, Bob) → false (Object.equals = порівняння посилань!) → новий елемент
System.out.println(map.size()); // 2! Два "однакові" ключі

Головне правило: Перевизначив hashCode — перевизначай і equals!

🟡 Middle Level

Практичні наслідки

1. Дублікати в HashMap:

  • Обидва User(id=1) потрапляють в один бакет (hashCode = 100)
  • При перевірці equals() порівнює посилання → різні об’єкти → різні ключі
  • HashMap допускає обидва

2. Неможливість отримання даних:

  • Поклали new User(1L) → дістати намагаємося по new User(1L)
  • hashCode правильний (той самий бакет)
  • equals() повертає false (різні посилання)
  • Результат: null

3. Підвищене споживання пам’яті:

  • HashSet містить більше елементів, ніж очікувалося. При великому потоці даних це призводить до підвищеного споживання пам’яті і потенційного OutOfMemoryError.

Порівняння двох ситуацій

Ситуація Де шукаються? Результат
equals OK, hashCode NO Різні бакети Не знаходить (шукає не там)
hashCode OK, equals NO Один бакет Не знаходить (не впізнає)

Наслідки для продуктивності

Однаковий hashCode для всіх ключів → всі елементи в одному бакеті → деградація до O(n)/O(log n). Плюс безглузді виклики equals().

🔴 Senior Level

Internal Mechanics

При map.get(new User(1L)):

// 1. Знаходимо бакет (правильний): index = (n-1) & hash = 100
// 2. Ітеруємо елементи в бакеті 100
// 3. Для кожного: p.hash == hash (true) → p.key.equals(key)
// 4. Object.equals(): this == obj → false (різні об'єкти)
// 5. Повертаємо null

Елемент фізично в правильному бакеті, але не впізнаний.

Memory Leak у production

Set<Event> uniqueEvents = new HashSet<>();
// hashCode перевизначено за eventId, equals — ні
for (Event e : stream) {
    uniqueEvents.add(e); // Кожен об'єкт додається!
}
// Результат: OOM при обробці великого потоку

Порівняння з Object.equals()

// Object.equals() — це просто:
public boolean equals(Object obj) { return this == obj; }

Це порівнює посилання, а не вміст. Навіть два new User(1L) — різні посилання → різні об’єкти.

Edge Cases

  1. Singleton-подібні об’єкти: Якщо завжди використовується один і той самий екземпляр, проблема не проявиться (але це крихке рішення)
  2. Десеріалізація: При десеріалізації завжди створюється новий об’єкт — проблема гарантована

Production Diagnostics

Ознаки проблеми:

  • Зростаючий розмір колекцій без очікуваного росту
  • containsKey() повертає false для “наявних” ключів
  • Heap dump показує безліч “однакових” об’єктів у Map

Best Practices

  1. Завжди генеруйте пару — використовуйте IDE або Lombok @EqualsAndHashCode
  2. Records — автоматично генерують обидва методи
  3. Статичний аналіз — SonarQube/SpotBugs знаходить це порушення

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

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

  • Без equals() об’єкти з однаковим hashCode потрапляють в один бакет, але Object.equals = порівняння посилань (==)
  • HashMap допускає дублікати: два new User(1L) = різні ключі
  • get() поверне null — бакет правильний, але equals не впізнає рівний об’єкт
  • OOM при обробці потоків: HashSet додає кожен об’єкт замість дедуплікації
  • Object.equals() = це просто this == obj, порівняння за посиланням

Часті уточнюючі питання:

  • Чим відрізняється від ситуації “equals OK, hashCode NO”? — там шукають не в тому бакеті, тут — в правильному, але не впізнають
  • Коли проблема не проявиться? — якщо завжди використовується один екземпляр (singleton)
  • Чому OOM? — uniqueEvents.add() додає кожен об’єкт, Set росте нескінченно
  • Десеріалізація погіршує? — так, завжди створюється новий об’єкт — проблема гарантована

Червоні прапорці (НЕ говорити):

  • «Це менш серйозна помилка ніж відсутність hashCode» — ні, обидві критичні
  • «equals можна не перевизначати для immutable об’єктів» — ні, навіть immutable об’єкти порівнюються за посиланням
  • «У production це не зустрічається» — зустрічається при десеріалізації

Пов’язані теми:

  • [[7. Що таке контракт equals() і hashCode()]]
  • [[10. Що станеться, якщо перевизначити equals() але не перевизначити hashCode()]]