Equality: best practices. Или как правильно переопределить метод equals.
Полный список в конце поста.
Базовая реализация в классе
Несмотря на кажущуюся простоту, в реализации
Странное поведение системы ➡️ неделя дебага по всей бизнес-логике ➡️ ошибка в
Простой способ не ошибиться — не переопределять
1️⃣ Для перечислений (
2️⃣ Когда важны сами экземпляры, а не данные внутри них. Пример - классы
3️⃣ Сравнение объектов вообще не предполагается. Пример - класс
4️⃣ В базовом классе уже есть
5️⃣ Класс имеет модификатор доступа
В остальных случаях его лучше переопределить. Во многих проектах при этом используется аннотация
Итак, при реализации
1️⃣ Рефлексивность — экземпляр должен быть равен сам себе.
2️⃣ Консистентность — если экземпляры равны, то без внешних воздействий они должны оставаться равны.
✅ Сравнивать только неизменяемые поля.
Чтобы обезопасить себя от проблем с
Если используете аннотацию
✅
Если в системе находятся объекты базового класса и его наследника нужно чётко понимать, как они будут между собой сравниваться.
Возьмём класс Точка, где равенство проверяется по координатам.
И его наследника — класс ЦветнаяТочка, где сравниваются координаты и цвет.
4️⃣ Транзитивность
✅ Для иерархий классов писать юнит-тесты на симметричность и транзитивность.
✅ Не использовать
🤔 Если в системе используются объекты базового и произвольного типов рассмотрите вариант замены наследования на композицию.
5️⃣ Сравнение существующего объекта с null должно возвращать false.
✅ Добавить отдельную проверку
1️⃣ Не переопределяйте
2️⃣ Используйте в
3️⃣ Для иерархии классов пишите тесты на симметричность и транзитивность.
4️⃣ Для ключей в
5️⃣ Не использовать
6️⃣ При использовании
7️⃣ Кратко проверить два поля на null и равенство можно через
9️⃣ Не забывайте про
Полный список в конце поста.
Базовая реализация в классе
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
.