Что случится, если изменить ключ после добавления в 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]]