Що станеться, якщо змінити ключ після додавання в HashMap?
Якщо змінити поля об'єкта-ключа після put(), HashMap не зможе знайти цей елемент. Об'єкт залишиться в карті, але стане "невидимим" — його неможливо отримати, оновити або видалити.
🟢 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
Покрокова механіка збою
- Додавання:
map.put(user, "Active")—user.hashCode() = 100→ бакет №100 - Зміна:
user.setName("New")—user.hashCode() = 200 - Пошук:
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(), змінити, вставити назад.
Типові помилки
- Зміна ID сутності — найчастіший кейс
- Зміна імені/email — якщо вони беруть участь в hashCode
- 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():
- Обчислює
hash(key)— використовує поточний hashCode - Йде в бакет за поточним індексом
- Не знаходить елемент (він в старому бакеті)
HashMap не зберігає “snapshot” хешу ключа. Кожен get() обчислює хеш заново.
Edge Cases
- Часткова зміна: Змінили поле, але hashCode не змінився (наприклад, поле не бере участі в hashCode) — працює, але крихко
- Відновлення: Якщо повернути поля в початковий стан — hashCode збіжеться, елемент “знайдеться”
- Конкурентна зміна: Один потік змінює ключ, інший шукає — 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]]