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

Що станеться, якщо змінити ключ після додавання в HashMap?

Якщо змінити поля об'єкта-ключа після put(), HashMap не зможе знайти цей елемент. Об'єкт залишиться в карті, але стане "невидимим" — його неможливо отримати, оновити або видалити.

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

🟢 Junior Level

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

Приклад:

MutableKey key = new MutableKey(1);
map.put(key, "Hello");  // Потрапив в бакет №1
key.setValue(2);        // Змінили ключ — хеш тепер інший
System.out.println(map.get(key));  // null!

// put(key, "Hello"): key.hashCode() = 1 → бакет #1
// key.setValue(2): key.hashCode() тепер = 2
// get(key): обчислює хеш = 2 → шукає в бакеті #2 → null!
// Елемент фізично в бакеті #1 — але get() шукає в #2

Аналогія: Ви змінили адресу проживання, але не повідомили банку. Банк надішле вам листа на стару адресу — ви його не отримаєте.

🟡 Middle Level

Покрокова механіка збою

  1. Додавання: map.put(user, "Active")user.hashCode() = 100 → бакет №100
  2. Зміна: user.setName("New")user.hashCode() = 200
  3. Пошук: map.get(user) — обчислює хеш 200 → бакет №200 → null

Парадокс: Ви маєте пряме посилання на об’єкт-ключ, але не можете дістати значення!

Видимість через ітератор

При ітерації по entrySet() ви побачите цей елемент:

for (Map.Entry<MutableKey, String> e : map.entrySet()) {
    System.out.println(e.getKey() + " = " + e.getValue());
    // Надрукується: MutableKey(id=2) = Hello
}

Ітератор проходить по всіх фізичних бакетах, тому знаходить “загублений” елемент.

Як виправити?

Найпростіший спосіб — створити нову карту:

Map<MutableKey, String> newMap = new HashMap<>(oldMap);
// При копіюванні для кожного ключа обчислюється актуальний хеш

Альтернатива: видалити старий ключ через ітератор entrySet(), змінити, вставити назад.

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

  1. Зміна ID сутності — найчастіший кейс
  2. Зміна імені/email — якщо вони беруть участь в hashCode
  3. JPA Entity як ключ — Entity змінюється при persist/merge

🔴 Senior Level

Ghost Entry Pattern

“Примарні” записи — це витік пам’яті, який:

  • Не детектується стандартними профілювальниками (об’єкт reachable)
  • Росте пропорційно кількості змін
  • Проявляється тільки через OutOfMemoryError
// У production: сервіс оновлює ключі і кладе нові версії
Map<ConfigKey, Config> cache = new HashMap<>();
// При кожному оновленні: старий ключ "губиться", новий додається
// Через 1000 оновлень: 999 ghost entries в пам'яті

Порівняння з IdentityHashMap

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

Але семантика інша: new Key(1) і new Key(1) — різні ключі.

Internal Mechanics

HashMap при get():

  1. Обчислює hash(key) — використовує поточний hashCode
  2. Йде в бакет за поточним індексом
  3. Не знаходить елемент (він в старому бакеті)

HashMap не зберігає “snapshot” хешу ключа. Кожен get() обчислює хеш заново.

Edge Cases

  1. Часткова зміна: Змінили поле, але hashCode не змінився (наприклад, поле не бере участі в hashCode) — працює, але крихко
  2. Відновлення: Якщо повернути поля в початковий стан — hashCode збіжеться, елемент “знайдеться”
  3. Конкурентна зміна: Один потік змінює ключ, інший шукає — race condition. Результат залежить від порядку виконання потоків: можливі загублені оновлення або stale reads (читання застарілих даних).

Production Monitoring

Для виявлення:

  • Порівняйте map.size() з кількістю унікальних ключів через ітератор
  • Використовуйте JFR для аналізу розподілу бакетів
  • Heap dump покаже ghost entries (елементи з “неправильним” хешем)

Defensive Strategy

// Pattern: Delete → Modify → Insert
Value v = map.remove(key);
key.modify();
map.put(key, v != null ? v : newValue);

Або використовуйте імутабельні ключі:

record Key(String id) {} // Не можна змінити

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

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

  • Елемент залишається в старому бакеті, get() шукає в новому → null
  • Ітератор entrySet() БАЧИТЬ «загублений» елемент — проходить всі фізичні бакети
  • Парадокс: є пряме посилання на ключ, але не можна дістати значення
  • HashMap не зберігає snapshot хеша — кожен get() обчислює hashCode заново
  • Ghost Entry Pattern: витік пам’яті, росте пропорційно змінам
  • Рішення: Delete → Modify → Insert паттерн, або імутабельні ключі

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

  • Чи можна видалити загублений елемент? — через ітератор entrySet() так, через remove() ні
  • Якщо повернути поля назад — знайдеться? — так, hashCode збіжеться, елемент «знайдеться»
  • Чим це відрізняється від mutable-key проблеми? — це приватний випадок: ключ вже був у Map, потім змінений
  • Чому ітератор бачить елемент? — ітератор проходить по всіх бакетах фізично, а не за хешем

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

  • «Елемент видалиться автоматично» — ні, залишається в пам’яті
  • «HashMap перезаписує елемент на новий бакет» — ні, залишається на старому місці
  • «Це безпечно якщо змінити назад» — крихке рішення, race condition в багатопотоковості

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

  • [[12. Чи можна використовувати змінюваний об’єкт як ключ в HashMap]]
  • [[14. Які вимоги до ключа HashMap]]
  • [[19. Що відбувається при rehashing]]