Почему важно, чтобы ключ в мапе не менялся после помещения
Что проверяют
Понимаете ли вы, как HashMap находит запись, и связываете ли контракт equals/hashCode с реальным поведением коллекции. Заодно смотрят, отличаете ли вы ситуацию «объект потерялся» от ситуации «объект удалён».
Как отвечать
При put мапа считает хеш ключа, домешивает к нему старшие биты и по результату выбирает корзину. Посчитанное значение сохраняется прямо в узле, поэтому при расширении таблицы запись раскладывается по старому хешу, а не по пересчитанному.
При get хеш считается заново от текущего состояния ключа. Если поле, участвующее в hashCode, изменили после put, новый хеш укажет на другую корзину, там записи нет, и метод вернёт null. Сама запись никуда не делась: она видна при обходе entrySet, учитывается в size, но недостижима ни через get, ни через containsKey, ни через remove. Это утечка, которой не видно по счётчику.
var key = new StringBuilder("a");
map.put(key, 1);
key.append("b"); // хеш ключа изменился
map.get(key); // nullОтсюда правило: ключ либо неизменяем, либо его equals и hashCode опираются только на неизменяемые поля. Безопасные варианты: String, обёртки примитивов, enum, record из неизменяемых полей, доменный идентификатор. В TreeMap та же беда с другой стороны: позиция определяется методом сравнения, и правка поля ломает порядок в дереве. HashSet внутри устроен как HashMap, так что правило распространяется и на него.
Где спотыкаются
Говорят, что при изменении ключа запись переедет в правильную корзину. Мапа не подписана на изменения ключа и узнать о них не может, а перекладывание происходит только при расширении таблицы и по ранее сохранённому хешу.
Переопределяют equals и забывают hashCode или наоборот. Тогда два логически равных ключа уходят в разные корзины, equals даже не вызывается, и в мапе спокойно появляется пара одинаковых по смыслу ключей.
Считают, что record или финальные поля дают гарантию. Не дают, если внутри лежит ссылка на изменяемый объект: List считает свой хеш по элементам, поэтому добавление элемента в список, который уже стал частью ключа, ломает поиск ровно так же.