Как изменение поля, участвующего в hashCode, после помещения объекта в HashSet влияет на последующий поиск этого объекта?
После изменения такого поля HashSet может перестать находить объект через contains и remove, хотя сам объект всё ещё находится внутри множества. Коллекция размещает элемент по хешу, вычисленному при добавлении, а при поиске использует уже новый хеш.
Это делает изменяемые объекты ненадёжными элементами HashSet и ключами HashMap, если изменяемое состояние участвует в equals или hashCode.
Хеш-таблицы были созданы для быстрого поиска элементов по ключу без последовательного перебора всей коллекции. В Java этот подход реализован в HashMap, а HashSet использует HashMap внутри и хранит элементы как его ключи.
Чтобы такой поиск был корректным, объект должен сохранять согласованность между equals и hashCode на протяжении времени, когда он находится в хеш-коллекции. Это ограничение является платой за быстрый доступ в среднем за константное время.
При добавлении объект помещается в корзину, выбранную на основе его текущего хеша. Если затем изменится поле, влияющее на hashCode, вычисленный хеш станет другим, и поиск начнётся уже с другой корзины.
В результате contains может вернуть false, а remove — не удалить объект. При этом объект не исчезает из коллекции: его можно увидеть при обходе, а повторное добавление логически того же объекта может привести к неожиданному состоянию множества.
HashSet фактически делегирует операции внутреннему HashMap. При добавлении вычисляется hashCode элемента, по нему выбирается корзина, а затем внутри корзины выполняется проверка равенства через equals.
При последующем поиске хеш вычисляется заново. Если значение изменилось, поиск обычно попадает в другую корзину и не доходит до прежней записи. Если новый хеш случайно приводит к той же корзине, операция может сработать, поэтому результат не следует считать гарантированным.
Контракт Java требует, чтобы если два объекта равны по equals, их hashCode совпадали. На практике для ключа также необходимо, чтобы значения, влияющие на equals и hashCode, не менялись, пока ключ находится в коллекции.
После изменения login объект остаётся в старой корзине, но contains вычисляет хеш для "bob". Надёжные варианты — использовать неизменяемые ключи, не включать изменяемое состояние в equals и hashCode либо сначала удалить объект, изменить его и затем добавить заново.
Удаление перед изменением безопасно только при условии, что удаление действительно успешно и объект не используется конкурентно. Для многопоточного кода дополнительно нужны подходящие средства синхронизации или потокобезопасные структуры; сама неизменяемость ключа не решает все вопросы видимости данных.
В сервисе пользователи хранились в HashSet, а поле email одновременно использовалось для отображения и сравнения пользователей. После подтверждения нового адреса приложение изменяло email прямо в уже сохранённом объекте. Проверка существования пользователя начинала возвращать false, а удаление старой записи не работало.
Рассматривались три варианта. Можно было удалять пользователя перед изменением и добавлять после него, но это требовало строгого контроля всех мест изменения. Можно было оставить идентичность пользователя основанной на изменяемом email, что сохраняло смысл модели, но делало её уязвимой для будущих изменений.
Выбрали неизменяемый технический идентификатор пользователя как основу equals и hashCode, а email оставили обычным изменяемым атрибутом. Это сохранило стабильность ключа, позволило менять контактные данные и устранило ошибки поиска в HashSet и HashMap.
Использует ли HashSet только hashCode и может ли он игнорировать equals?
Нет. Хеш нужен для выбора корзины и быстрого сокращения области поиска. Если в корзине уже есть элементы с тем же хешем, коллекция дополнительно проверяет равенство через equals. Поэтому разные объекты с одинаковым хешем могут находиться в одном HashSet, если они не равны.
Что произойдёт, если у двух неравных объектов одинаковый hashCode?
Это допустимая хеш-коллизия. HashSet сохранит оба объекта, потому что после совпадения хеша проверка equals покажет, что они различаются. Плохое распределение хешей ухудшит производительность, но само по себе не нарушает корректность коллекции.
Достаточно ли использовать record, чтобы ключ гарантированно был безопасным для HashMap?
Нет. record автоматически создаёт equals и hashCode на основе компонентов, но это не делает вложенные объекты неизменяемыми. Если компонентом является изменяемый объект, его состояние может повлиять на хеш записи. Поэтому компоненты ключевого record должны быть фактически стабильными либо защищёнными от внешней мутации.