В сервисе Java ключ объекта, уже добавленного в HashMap, изменили по полю, участвующему в equals() и hashCode(). Что произойдет при последующем поиске этого ключа?
Поиск, как правило, вернет null, хотя запись физически останется в HashMap. После изменения ключ может вычислить другой хеш и попасть в другой внутренний бакет, поэтому карта не найдет прежнюю запись.
Такой ключ нарушает контракт HashMap: значения, влияющие на equals() и hashCode(), нельзя изменять, пока объект используется как ключ.
HashMap реализует ассоциативный массив поверх хеш-таблицы. Такой подход появился для получения быстрого среднего времени операций put, get и remove без последовательного просмотра всех ключей.
Для этого карта вычисляет хеш ключа, преобразует его в индекс внутреннего бакета, а затем сравнивает ключи через equals(). Корректность механизма зависит от того, что ключ сохраняет логическую идентичность во время хранения в карте.
При добавлении записи HashMap сохраняет ключ и значение в конкретном бакете. Если после этого изменить поле, участвующее в hashCode(), повторный поиск вычислит уже другой хеш и обычно проверит другой бакет.
Запись не исчезает из памяти, но становится практически недоступной через обычные операции поиска по этому ключу. Это может привести к ошибкам в кэше, невозможности удалить запись, росту потребления памяти и накоплению устаревших данных.
Упрощенно операция выглядит так: HashMap получает key.hashCode(), применяет внутреннее преобразование хеша, выбирает бакет, а затем ищет подходящий ключ по хешу и equals(). При изменении значимого состояния ключа результат hashCode() может измениться, поэтому карта ищет запись не там, где она была размещена.
В примере используется record: его состояние после создания нельзя изменить, поэтому он безопасен как ключ при условии, что содержащиеся внутри него компоненты также не нарушают это свойство. Для изменяемого класса безопасный вариант — удалить запись до изменения ключа, изменить объект и добавить его заново.
Важно, что проблема возникает не только при смене хеша. Даже если хеш случайно остался тем же, изменение полей, участвующих в equals(), может сделать сравнение с сохраненным ключом ложным.
Обычно ключи делают неизменяемыми: используют String, числовой идентификатор, record или класс с final-полями и корректными equals()/hashCode(). Нельзя исправить проблему простым переопределением только hashCode() или только equals() — эти методы должны согласованно описывать одну логическую идентичность.
В кэше профилей ключом был изменяемый объект запроса. После обновления параметра запроса часть ранее помещенных записей перестала находиться, а очистка кэша по исходным объектам не удаляла их.
Рассматривались три варианта:
Выбрали третий вариант: перед помещением в кэш создавался неизменяемый ключ с нужными параметрами. В результате поиск и удаление стали детерминированными, а изменение исходного объекта запроса больше не влияло на содержимое карты.
Нет, не обязательно. Пока HashMap содержит ссылку на запись и ее ключ, объект достижим из карты и сборщик мусора не удалит его.
Такая запись может оставаться в карте до явного удаления, вызова clear() или удаления через итератор. Поэтому ошибка способна проявляться как утечка логических записей и рост памяти, даже если приложение больше не может найти эти записи через get().
map.get(key) после изменения ключа вернет null?Нет, слово «всегда» было бы неточным. Если изменение не повлияло на результат hashCode() и не нарушило сравнение equals(), поиск может продолжить работать.
Однако полагаться на это нельзя: контракт ключа требует стабильности значимых для равенства данных. Даже при неизменном хеше изменение equals() способно сделать запись недоступной, а совпадение хеша не гарантирует успешный поиск.
Да, при строгом соблюдении этого условия. Ключ должен оставаться неизменным для всех операций, пока запись находится в карте; после удаления его можно изменить и использовать снова.
На практике неизменяемый тип предпочтительнее, потому что ограничение проверяется конструкцией класса, а не дисциплиной всех вызывающих мест. Если изменение неизбежно, надежный порядок действий — удалить запись по старому состоянию, изменить ключ и добавить запись заново.