Как обычно предотвращают изменение ключей в хэшмапе после помещения
Что проверяют
Понимаете ли вы, что HashMap запоминает хэш ключа в момент put, и знаете ли, как это защищают на уровне дизайна класса. Следом обычно просят показать, что именно сломается, если ключ всё же изменить.
Как отвечать
Сначала механика. При put карта вычисляет hashCode() ключа, выбирает бакет и сохраняет хэш в узле. Если потом поменять поле, от которого зависят hashCode() и equals(), то get с тем же объектом посчитает новый хэш, пойдёт в другой бакет и вернёт null. Запись никуда не делась: занимает память, видна при итерации, но по ключу недостижима. containsKey врёт, remove не срабатывает, получается утечка.
Как это предотвращают:
- неизменяемые ключи:
String,Integer,UUID,LocalDate, перечисления. Свой ключ делают классом сprivate finalполями, без сеттеров, объявленнымfinal, чтобы наследник не добавил изменяемое состояние; record: с Java 16 самый короткий способ.equals()иhashCode()генерируются по компонентам, поля финальные. Но неизменяемость поверхностная:recordс полемListменяется через этот список, поэтому копируйте его черезList.copyOf()в компактном конструкторе;- защитные копии: если ключ собирается из изменяемых данных (массив,
Date, коллекция), копируйте их при создании ключа; - ключ по идентификатору: вместо изменяемой сущности кладите в карту её неизменный
id; - соглашение: если класс изменяемый,
hashCode()иequals()зависят только от полей, которые не меняются после создания.
Можно упомянуть IdentityHashMap: он сравнивает по ссылке и от полей не зависит, но это другая семантика, а не лечение.
Где спотыкаются
Говорят, что при изменении ключа запись удаляется или карта сама её перехеширует. Ни то, ни другое: запись остаётся на старом месте.
Считают final у ссылки гарантией неизменяемости. final List<String> меняется через add, и хэш ключа поплывёт.
Переопределяют equals() без hashCode() или наоборот. Тогда карта перестаёт находить равные по смыслу ключи даже без всяких изменений.