Почему элемент может не находиться в HashMap при правильном переопределении hashCode и equals
Что проверяют
Понимаете ли вы, что соблюдение контракта hashCode и equals это условие необходимое, но не достаточное. Настоящая тема вопроса это неизменяемость ключа и то, что происходит с элементом после его мутации.
Как отвечать
Главная причина одна: ключ изменили после вставки. Бакет вычисляется в момент put, объект физически лежит там. Меняем поле, участвующее в hashCode, и поиск считает новый хэш, идёт в другой бакет и ничего не находит. Элемент при этом есть в size() и виден при обходе.
var key = new Point(1, 2);
map.put(key, "a");
key.setX(5);
map.get(key); // nullДальше стоит назвать менее очевидные случаи:
- расширение таблицы. При росте элементы раскладываются по новым хэшам, и испорченный ключ уезжает окончательно. Поведение выглядит плавающим: до расширения иногда находилось, после перестало;
- ищут не тем объектом.
getпринимаетObject, компилятор молчит, аIntegerиLongс одним значением не равны. Тот же класс ошибок сBigDecimal, у которогоequalsучитывает масштаб, и с массивом в роли ключа, где сравниваются ссылки; - конкурентный доступ. Обычный
HashMapиз нескольких потоков теряет записи при расширении, для этого естьConcurrentHashMap.
Вывод в ответе: ключ должен быть неизменяемым, record или String, либо в хэш входят только те поля, которые точно не поменяются.
Где спотыкаются
Сводят всё к коллизиям. Совпадение хэшей это штатная ситуация, она разрешается списком или деревом внутри бакета и на корректность не влияет. Ломает именно смена хэша у уже лежащего ключа.
Считают, что достаточно переопределить equals. Без согласованного hashCode объект попадёт в другой бакет, и до сравнения дело просто не дойдёт.
Разделяют HashSet и HashMap. Первый внутри держит второй, поэтому грабли те же: объект остаётся в наборе, но contains возвращает false.