Как сравнить объекты в Java не по ссылкам и какой контракт нужно соблюдать
Что проверяют
Знаете ли вы контракт equals() и hashCode() целиком, а не только правило «переопределяйте оба». Следом просят написать equals() вживую и спрашивают, что будет с HashMap, если контракт нарушен.
Как отвечать
По умолчанию Object.equals() сравнивает ссылки, как ==. Для сравнения по значению метод переопределяют, и контракт из документации Object требует:
- рефлексивность:
x.equals(x)всегдаtrue; - симметричность:
x.equals(y)совпадает сy.equals(x); - транзитивность: из
x.equals(y)иy.equals(z)следуетx.equals(z); - согласованность: пока поля, участвующие в сравнении, не менялись, повторные вызовы дают тот же результат;
null:x.equals(null)возвращаетfalse, а не бросает исключение.
К этому прилагается контракт hashCode(): равные объекты обязаны давать одинаковый хэш, обратное не требуется. Если переопределить только equals(), два равных объекта попадут в разные корзины HashMap, и map.get() вернёт null.
Каркас реализации: проверка this == o, проверка типа, приведение, сравнение значимых полей через Objects.equals() для ссылок и == для примитивов (для double лучше Double.compare()). Хэш через Objects.hash() по тем же полям.
Проговорите выбор между getClass() и instanceof: instanceof пропускает наследников и ломает симметричность, если подкласс добавит поле, а getClass() честнее, но мешает подстановке наследника. С Java 16 для объектов-значений есть record, где эти методы генерируются по всем компонентам. compareTo() желательно согласовывать с equals(): у BigDecimal они расходятся, и TreeSet с HashSet ведут себя по-разному.
Где спотыкаются
Пишут equals(MyClass other) вместо equals(Object o). Это перегрузка, а не переопределение, и коллекции её не вызывают. Аннотация @Override поймала бы ошибку при компиляции.
Строят hashCode() на изменяемых полях и меняют объект после того, как положили его в HashSet. Элемент остаётся в старой корзине: contains() его не находит, remove() не удаляет.
Сравнивают строки и обёртки через ==. Для Integer это работает только в диапазоне кэша от -128 до 127, а дальше внезапно ломается.