Вопрос 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]]