Полный список в конце поста.
Базовая реализация в классе
Object проверяет только равенство ссылок - по умолчанию каждый экземпляр класса равен только самому себе. Часто рекомендуют для новых классов переопределять метод equals и проверять равенство по значимым полям.Несмотря на кажущуюся простоту, в реализации
equals есть несколько тонких моментов. Кода в этом методе мало, выглядит он просто, а тесты на равенство редко кто пишет. Тем не менее часто встречается ситуация:Странное поведение системы ➡️ неделя дебага по всей бизнес-логике ➡️ ошибка в
equals ➡️ гнев и разочарование.Простой способ не ошибиться — не переопределять
equals вообще. Иногда это уместно:1️⃣ Для перечислений (
enum)2️⃣ Когда важны сами экземпляры, а не данные внутри них. Пример - классы
Thread, RecursiveTask.3️⃣ Сравнение объектов вообще не предполагается. Пример - класс
Pattern.4️⃣ В базовом классе уже есть
equals и нам подходит эта реализация.5️⃣ Класс имеет модификатор доступа
private или по умолчанию и мы уверены, что метод equals не будет вызван.В остальных случаях его лучше переопределить. Во многих проектах при этом используется аннотация
@EqualsAndHashCode библиотеки lombok, ограничения при её использовании мы тоже рассмотрим.Итак, при реализации
equals нужно учитывать следующие свойства:1️⃣ Рефлексивность — экземпляр должен быть равен сам себе.
2️⃣ Консистентность — если экземпляры равны, то без внешних воздействий они должны оставаться равны.
✅ Сравнивать только неизменяемые поля.
Чтобы обезопасить себя от проблем с
equals по возможности используйте для ключей в Map не классы целиком, а их неизменяемые поля, например, id.Если используете аннотацию
@EqualsAndHashCode явно указывайте поля, по которым идёт сравнение. Это поможет избежать ошибок при добавлении в класс изменяемого поля.✅
@EqualsAndHashCode(of={“x”,“y”})
3️⃣ Симметрия.Если в системе находятся объекты базового класса и его наследника нужно чётко понимать, как они будут между собой сравниваться.
Возьмём класс Точка, где равенство проверяется по координатам.
И его наследника — класс ЦветнаяТочка, где сравниваются координаты и цвет.
Точка обычная=new Точка(1,1);
Точка красная=new ЦветнаяТочка(1,1,RED);
Тогда обычная.equals(красная) будет true, а результат красная.equals(обычная) зависит от требований к системе — нужно ли игнорировать цвет в этом случае или нет.4️⃣ Транзитивность
.
Это свойство тоже легко нарушить, если мы имеем дело с иерархией:Точка красная=new ЦветнаяТочка(1,1,RED);
Точка обычная=new Точка(1,1);
Точка синяя=new ЦветнаяТочка(1,1,BLUE);
Если мы решили игнорировать цвет при сравнении разных типов, то допустима такая ситуация:красная.equals(обычная) - true обычная.equals(синяя) — trueкрасная.equals(синяя) - true 😥✅ Для иерархий классов писать юнит-тесты на симметричность и транзитивность.
✅ Не использовать
@EqualsAndHashCode для иерархий классов.🤔 Если в системе используются объекты базового и произвольного типов рассмотрите вариант замены наследования на композицию.
5️⃣ Сравнение существующего объекта с null должно возвращать false.
✅ Добавить отдельную проверку
if(o == null)
Итого:1️⃣ Не переопределяйте
equals, если сравнение не предполагается.2️⃣ Используйте в
equals только неизменяемые поля.3️⃣ Для иерархии классов пишите тесты на симметричность и транзитивность.
4️⃣ Для ключей в
Map желательно использовать неизменяемые поля, например, ID.5️⃣ Не использовать
@EqualsAndHashCode для иерархии классов.6️⃣ При использовании
@EqualsAndHashCode явно прописывайте поля.7️⃣ Кратко проверить два поля на null и равенство можно через
Objects.equals(this.a, а)
8️⃣ Проверяйте первыми поля, которые вероятнее всего отличаются.9️⃣ Не забывайте про
hashcode — используйте те же поля, что в методе equals.