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

Чи можна використовувати змінюваний об'єкт як ключ в HashMap?

Технічно — так, можна, але не рекомендується. Якщо змінити поля об'єкта-ключа після додавання в HashMap, ви не зможете знайти цей елемент.

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

🟢 Junior Level

Технічно — так, можна, але не рекомендується. Якщо змінити поля об’єкта-ключа після додавання в HashMap, ви не зможете знайти цей елемент.

Проста аналогія: Ви поклали листа в поштову скриньку №5. Потім перефарбували скриньку в інший колір і поміняли номер на №10. Листоноша шукатиме за новим номером і не знайде листа.

Приклад проблеми:

public class MutableKey {
    private int id; // Не final! Можна міняти

    public void setId(int id) { this.id = id; }

    @Override public int hashCode() { return id; }
    @Override public boolean equals(Object o) { /* за id */ }
}

MutableKey key = new MutableKey(1);
map.put(key, "Value"); // Потрапив в бакет №1
key.setId(2);          // Хеш тепер = 2
System.out.println(map.get(key)); // null! Ключ "зламаний"

Рішення: Використовуйте незмінювані (immutable) об’єкти як ключі — String, Integer, Record.

🟡 Middle Level

Проблема “зламаного ключа” (Broken Key)

HashMap “запам’ятовує” положення об’єкта за його hashCode() в момент put(). Якщо хеш змінюється — елемент залишається в старому бакеті, але пошук йде в новому.

Механізм збою:

  1. key.hashCode() = 100put(key, "Value") → бакет №100
  2. key.setId(2)hashCode() = 200
  3. get(key) → шукає в бакеті №200 → null
  4. remove(key) → теж не знайде → витік пам’яті

Чи можна міняти поля, що не входять в hashCode/equals?

Технічно так, але це bad practice:

  • Вводить в оману інших розробників
  • Ризик: в майбутньому ці поля можуть бути додані в контракт

Як робити правильно

Підхід Приклад
Імутабельні класи String, Integer, LocalDate
Record (Java 14+) public record Key(String id) {}
Final поля private final String id;
Delete-Update-Insert Видалити → змінити → додати назад

Типові помилки

  1. Використання Entity JPA як ключа — Entity змінюється в процесі роботи
  2. StringBuilder як ключ — мутабельний: його вміст можна змінити після додавання в Map, що зламає hashCode. String гарантує незмінюваність.
  3. Колекції як ключArrayList мутабельний, використовуйте обгортку

🔴 Senior Level

Memory Leak Pattern

Map<MutableKey, LargeObject> cache = new HashMap<>();
// Ключі змінюються і "губляться" — не видалити, не знайти
// Old Gen заповнюється → Full GC → OutOfMemoryError

“Примарні” записи займають пам’ять, але недоступні. У високонавантажених системах це призводить до деградації за години/дні.

IdentityHashMap як обхідний шлях

Map<MutableKey, Value> map = new IdentityHashMap<>();
// Використовує System.identityHashCode() і == замість equals/hashCode
// Адреса об'єкта не змінюється при зміні полів

⚠️ Але це змінює семантику: два різних об’єкти з однаковими полями = різні ключі. НЕ використовуйте IdentityHashMap, якщо вам потрібне змістовне порівняння ключів (два різних об’єкти з однаковими полями повинні вважатися одним ключем). Це рішення тільки для випадків, де важлива саме ідентичність посилання.

Internal Mechanics

HashMap не відстежує зміни ключів. Вона:

  • Не копіює ключ (зберігає посилання)
  • Не валідує хеш при get/put
  • Повністю довіряє поточному hashCode() ключа

Edge Cases

  1. Конкурентна зміна: Один потік змінив ключ, інший шукає — race condition
  2. Серіалізація: При десеріалізації створюється новий об’єкт — старий “привид” залишається
  3. WeakHashMap: Якщо значення посилається на ключ — циклічна залежність, витік

Production Diagnostics

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

  • Зростаючий розмір Map без відповідності бізнес-логіці
  • get() повертає null для “точно наявних” ключів
  • Ітератор знаходить елементи, які недоступні через get()

Best Practices для Highload

  1. Завжди final поля в ключах: private final String id;
  2. Records — компілятор гарантує імутабельність
  3. Defensive copy при отриманні ключа в конструкторі
  4. Статичний аналіз — забороняти mutable-ключі через code review

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

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

  • Технічно можна, але НЕ рекомендується: зміна полів ключа = «зламаний ключ»
  • HashMap запам’ятовує бакет за hashCode() при put; при зміні — шукає в іншому місці
  • get() = null, remove() не знайде → ghost entry, витік пам’яті
  • IdentityHashMap як обхідний шлях (використовує == і identityHashCode), але змінює семантику
  • Найкращі ключі: String, Integer, Record (immutable за дизайном)
  • StringBuilder/ArrayList мутабельні — не використовувати як ключі

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

  • Чи можна міняти поля, що не входять в hashCode? — технічно так, але bad practice
  • Як виправити mutable ключ? — видалити → змінити → вставити назад, або перестворити Map
  • Чому JPA Entity поганий ключ? — Entity змінюється при persist/merge
  • Що таке Ghost Entry Pattern? — записи займають пам’ять, але недоступні через get

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

  • «Можна використовувати mutable об’єкт якщо не міняти його» — крихке рішення
  • «IdentityHashMap вирішує всі проблеми» — ні, змінює семантику на identity-based
  • «HashMap відстежує зміни ключів» — ні, не відстежує

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

  • [[13. Що станеться, якщо змінити ключ після додавання в HashMap]]
  • [[14. Які вимоги до ключа HashMap]]
  • [[15. Чому String частіше використовується як ключ в HashMap]]