В HashSet два разных объекта равны по equals, но возвращают разные hashCode. Какое наблюдаемое последствие следует ожидать?
Нарушение контракта equals/hashCode делает поведение HashSet некорректным: коллекция может принять два равных объекта, а проверка наличия или удаление равного объекта могут не сработать. Одинаковый результат equals обязан означать одинаковый hashCode.
Хеш-коллекции появились для быстрого поиска элементов без последовательного сравнения со всеми объектами. Сначала hashCode определяет бакет, а затем equals проверяет равенство с объектами внутри выбранного бакета.
Поэтому контракт связывает два метода: равные объекты должны попадать в один и тот же логический участок таблицы. Сам по себе хеш не доказывает равенство, но его несовпадение позволяет коллекции не выполнить необходимое сравнение.
Если равные объекты имеют разные хеш-коды, HashSet может разместить их в разных бакетах и не обнаружить дубликат. В результате размер множества окажется больше ожидаемого, а операции contains и remove могут вернуть отрицательный результат для объекта, который логически уже находится в коллекции.
Это не особенность конкретной реализации, на которую можно безопасно опираться, а следствие нарушения обязательного контракта объектов, используемых в хеш-коллекциях.
При добавлении элемента HashSet фактически использует внутренний HashMap. Хеш определяет предполагаемый бакет, после чего коллекция сравнивает ключи через equals только среди кандидатов в этом бакете.
Если a.equals(b) возвращает true, но a.hashCode() и b.hashCode() различаются, второй объект может попасть в другой бакет. До сравнения через equals дело не дойдёт, поэтому множество сочтёт объекты различными.
Оба объекта логически равны по id, но имеют разные хеш-коды, поэтому множество может содержать их одновременно. Корректная реализация должна вычислять hashCode исключительно из тех же значимых полей, которые участвуют в equals.
Если эти поля изменяемы, после помещения объекта в HashSet их также нельзя менять: это отдельный риск потери объекта из-за изменения его бакета. Для объектов-значений обычно используют неизменяемые поля; если нужна идентичность именно экземпляров, следует явно выбрать семантику сравнения по ссылке, а не имитировать её нарушением контракта.
В системе множество заявок стало содержать дубликаты. Анализ показал, что equals сравнивал номер заявки, а hashCode включал ещё и дату создания. Два объекта одной заявки считались равными, но направлялись в разные бакеты.
Рассматривались три варианта: убрать проверку дубликатов, заменить HashSet на список или исправить контракт. Первый вариант скрывал ошибку, второй ухудшал поиск с амортизированной сложности около O(1) до O(n), а исправление hashCode сохраняло ожидаемую производительность и корректность.
Выбрали третий вариант: и equals, и hashCode стали использовать только неизменяемый номер заявки. После этого множество стало корректно подавлять дубликаты, а операции поиска сохранили ожидаемую постоянную сложность при нормальном распределении хешей.
1. Достаточно ли, чтобы одинаковый hashCode гарантировал equals?
Нет. Требование одностороннее: если объекты равны по equals, их хеш-коды обязаны совпадать. Обратное неверно: разные объекты могут иметь одинаковый хеш, что называется коллизией. В этом случае коллекция сравнит их через equals и сохранит оба, если они не равны.
2. Что произойдёт, если два равных объекта уже добавлены при нарушенном контракте, а затем hashCode исправить?
Уже размещённые записи автоматически не перестроятся. Изменение методов в коде действует для новых операций и новых экземпляров, но существующая таблица может быть заполнена по старым правилам; такие данные обычно нужно пересоздать в новой коллекции.
3. Почему нельзя переопределить только equals, оставив hashCode от Object?
Переопределённый equals начнёт считать логически равные объекты одинаковыми, а унаследованный Object.hashCode обычно даст им разные значения. Это классическое нарушение контракта, из-за которого хеш-коллекции не смогут надёжно реализовать уникальность и поиск. Если переопределён equals, согласованный hashCode должен быть частью той же модели равенства.