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

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

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

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

Junior Level

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

List<String> key = new ArrayList<>(List.of("A"));
Map<List<String>, String> map = new HashMap<>();
map.put(key, "value");

key.add("B"); // Змінили ключ!
System.out.println(map.get(key)); // null — не знайшли!

Чому

  • При put HashMap обчислила хеш ["A"] → поклала в бакет №5
  • Після зміни ключа хеш ["A", "B"] → інший бакет
  • get шукає в неправильному бакеті → null

Об’єкт все ще лежить в мапі, але ви не можете його дістати.


Middle Level

Механіка збою

  1. put(key, value) — хеш ключа → бакет №5
  2. Мутація — змінили поле ключа → новий хеш
  3. get(key) — новий хеш → бакет №10 → порожньо → null

Фізично об’єкт все ще в бакеті №5, але став недосяжним.

Наслідки

Логічна недоступність: об’єкт неможливо отримати через get() або видалити через remove() за зміненим ключем. Він займає місце до очищення всієї карти або перебору entrySet().

Дублікати в HashSet:

Set<List<String>> set = new HashSet<>();
set.add(new ArrayList<>(List.of("A")));
// Змінили елемент всередині списку
set.add(new ArrayList<>(List.of("A"))); // Додасться знову!

Як знайти загублений об’єкт

Через повний перебір — O(n) замість O(1):

for (var entry : map.entrySet()) {
    if (entry.getKey().contains("A")) {
        // знайшли
    }
}

Senior Level

Як уникнути

  1. Незмінні ключіString, UUID, Record — завжди
  2. Видалити → змінити → додати:
    V value = map.remove(key);
    key.mutate();
    map.put(key, value);
    
  3. IdentityHashMap — якщо мутація неминуча і ви не контролюєте об’єкт (порівняння за ==)
    Map<List<String>, String> map = new IdentityHashMap<>();
    // Порівняння за ==, а не за equals — мутація не ламає lookup, доки об'єкт той самий.
    

Глибинна проблема

Зміна мутабельного ключа порушує інваріант HashMap: hashCode ключа повинен бути стабільним. Це не баг HashMap, а порушення контракту hashCode() — якщо equals не змінився, то hashCode повинен бути тим самим, але мутація змінює і те, й інше.

Резюме для Senior

  • Зміна мутабельного ключа “ламає” мапу — об’єкт недоступний для get()
  • Це класичний memory leak та джерело важковловимих багів
  • Завжди проектуйте ключі як незмінні сутності
  • Якщо мутація необхідна: removemutateput
  • IdentityHashMap — крайній випадок, коли контроль над ключем неможливий

Шпаргалка для інтерв’ю

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

  • Зміна мутабельного ключа → новий hashCode → get шукає в іншому бакеті → null
  • Об’єкт фізично в мапі, але став недосяжним — логічний витік пам’яті
  • Дублікати в HashSet: змінений об’єкт додасться знову як “новий”
  • Знайти загублений об’єкт можна тільки через перебір entrySet — O(n) замість O(1)
  • Як уникнути: незмінні ключі завжди, або remove → mutate → put
  • IdentityHashMap — порівнює за ==, мутація не ламає lookup (доки об’єкт той самий)

Часті уточнювальні запитання:

  • Об’єкт видаляється з мапи? — Ні, залишається в старому бакеті, але недоступний через get/remove
  • Як знайти загублений об’єкт? — Перебір entrySet — O(n)
  • IdentityHashMap вирішує проблему? — Так, порівнює за посиланням, але це специфічний use case
  • Це баг HashMap? — Ні, порушення контракту: hashCode повинен бути стабільним

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

  • «Об’єкт видалиться з мапи» — ні, залишається але недоступний
  • «HashMap сама виправить hashCode» — ні, це контракт розробника
  • «Мутабельний ключ — це нормально» — класичний source of memory leaks і bugs
  • «IdentityHashMap — заміна HashMap» — це зовсім інша структура, порівнює за посиланням

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

  • [[27. Чи можна використовувати незмінні об’єкти як ключі в HashMap]]
  • [[22. В чому перевага незмінних об’єктів для кешування]]
  • [[20. Що таке Record і як він допомагає створювати незмінні класи]]
  • [[14. В чому різниця між shallow copy та deep copy]]