Лучшая практика выбора полей для реализации equals()

При написании юнит-тестов я часто сталкиваюсь с ситуацией, когда equals() для какого-то объекта в тестах -- в assertEquals -- должно работать иначе, чем в реальном окружении. Возьмем, к примеру, какой-нибудь интерфейс ReportConfig. Он имеет id и несколько других полей. По логике один конфиг равен другому, когда совпадают их id. Но когда дело доходит до тестирования какой-то конкретной реализации, скажем, XmlReportConfig, очевидно, что я хочу сопоставить все поля. Одно из решений — не использовать equals в тестах, а просто перебирать свойства или поля объекта и сравнивать их, но это не кажется хорошим решением.

Итак, помимо этого конкретного типа ситуаций, я хочу разобраться, каковы лучшие практики реализации equals, семантически, а не технически.


person Andrey Balaguta    schedule 16.03.2012    source источник
comment
When writing unit-tests, I often face the situation when equals() for some object in tests -- in assertEquals -- should work differently from how it works in actual environment. Зачем тебе это?   -  person helpermethod    schedule 16.03.2012
comment
Потому что есть тождество и равенство. Во многих ситуациях идентификации просто достаточно, особенно в приложениях БД. Однако при тестировании легко могут возникнуть ситуации, когда вы, возможно, клонируете предыдущую копию для сравнения с последующей копией, и в этих случаях идентичности недостаточно, вы хотите углубиться в фактические поля объект.   -  person Will Hartung    schedule 16.03.2012


Ответы (5)


каковы наилучшие методы реализации equals, семантически, а не технически.

В Java метод equals действительно следует рассматривать как " identity equals" из-за того, как он интегрируется с реализациями Collection и Map. Рассмотрим следующее:

 public class Foo() {
    int id;
    String stuff;
 }

 Foo foo1 = new Foo(10, "stuff"); 
 fooSet.add(foo1);
 ...
 Foo foo2 = new Foo(10, "other stuff"); 
 fooSet.add(foo2);

Если Foo identity является полем id, то второе fooSet.add(...) не должно не добавлять еще один элемент в Set, но должно возвращать false, так как foo1 и foo2 имеют одинаковые id. Если вы определяете метод Foo.equals (и hashCode) для включения оба поля id и stuff, это может быть нарушено, поскольку Set может содержать 2 ссылки на объект с одним и тем же полем id.

Если вы не храните свои объекты в Collection (или Map), то вам не нужно определять метод equals таким образом, однако многие считают это дурным тоном. Если в будущем вы сделаете сохранение его в Collection, тогда все будет сломано.

Если мне нужно проверить равенство всех полей, я обычно пишу другой метод. Что-то вроде equalsAllFields(Object obj) или что-то в этом роде.

Тогда вы бы сделали что-то вроде:

assertTrue(obj1.equalsAllFields(obj2));

Кроме того, рекомендуется не определять equals методы, учитывающие изменяемые поля. Проблема также усложняется, когда мы начинаем говорить об иерархии классов. Если дочерний объект определяет equals как комбинацию своих локальных полей и базового класса equals, то его симметрия нарушена:

 Point p = new Point(1, 2);
 // ColoredPoint extends Point
 ColoredPoint c = new ColoredPoint(1, 2, Color.RED);
 // this is true because both points are at the location 1, 2
 assertTrue(p.equals(c));
 // however, this would return false because the Point p does not have a color
 assertFalse(c.equals(p));

Еще одно чтение, которое я настоятельно рекомендую, — это раздел «Подводный камень № 3: определение равенства с точки зрения изменяемых полей» на этой замечательной странице:

Как написать метод равенства на Java

Некоторые дополнительные ссылки:

Да, и просто для потомков, независимо от того, какие поля вы выбрали для сравнения для определения равенства, вам нужно использовать одни и те же поля в расчете hashCode. equals и hashCode должны быть симметричны. Если два объекта равны, они должны иметь одинаковый хэш-код. Обратное не обязательно верно.

person Gray    schedule 16.03.2012
comment
Если вы не храните свои объекты в коллекции, вам не нужно определять метод equals на основе идентичности. Весь смысл объектно-ориентированного программирования в том, что ваш класс представляет собой независимую абстракцию. Вы бы просачивали требования и настраивали интерфейс своего класса для определенных целей, что может сделать его бессмысленным. - person DPM; 16.03.2012
comment
Несмотря на академическую правильность @Jubbat, Java Collection влияет практически на каждую часть JDK. Вы можете игнорировать их, чтобы быть пуристом ООП, но вы делаете это на свой страх и риск. - person Gray; 16.03.2012
comment
Все еще думаю, что лучше представлять инварианты, такие как уникальные идентификаторы, в документации класса и использовать для них утверждения (см. мой ответ ниже) - person DPM; 16.03.2012
comment
@Джуббат. Мы не согласны с этим и утверждает. Прочитайте эту ссылку для получения дополнительной информации. artima.com/lejava/articles/equality.html - person Gray; 16.03.2012
comment
Я согласен со статьей. Какая часть противоречит тому, что я написал? - person DPM; 16.03.2012
comment
Хорошо, я понимаю: определение равенства с точки зрения изменяемых полей. Возможно, вы правы. Я должен подумать об этом... - person DPM; 16.03.2012
comment
@ Джуббат Да. Кроме того, взгляните на проблемы иерархии классов, связанные с равенством и симметрией. Эта проблема распространяется и на постоянство базы данных, а также на другие области. - person Gray; 16.03.2012
comment
Что касается иерархии классов, следует вообще избегать реализации equals или устранять иерархию в пользу композиции. Просто невозможно иметь иерархию, в которой у вас есть важная новая переменная в дочернем классе и метод equals, который привязывается к ее контракту. Но это другое дело того, что я изначально утверждал. - person DPM; 16.03.2012
comment
@Jubbat, в последнем случае ваша точка зрения верна для очевидных абстракций, таких как Point и Circle, но как насчет не столь очевидных случаев? Например, у нас есть ReportConfig базовый интерфейс, который определяет id и несколько других свойств. И затем у нас есть FileBasedReportConfig, у которого есть дополнительное свойство templateFile. Имеет ли смысл equals в этом случае? Конечно. Как это реализовать правильно - я не уверен... - person Andrey Balaguta; 17.03.2012
comment
@AndreyBalaguta: Концептуально можно определить равенство, сказав, что любые законные производные типа, которые могут сравниваться равными чему-то другому производному этого типа, должны иметь каноническое представление, в которое его можно преобразовать, и две вещи независимо от типа являются равны, если они дадут одно и то же каноническое представление. Например, можно иметь абстрактный тип MatrixOfDouble с подтипами ArrayBackedMatrixOfDouble, DiagonalMatrixOfDouble и ZeroMatrixOfDouble и указать, что каноническая форма для любого из них представляет собой набор ячеек. - person supercat; 12.11.2013
comment
@AndreyBalaguta: Учитывая DiagonalMatrixOfDouble, диагональ которого содержит 500 значений, и ZeroMatrixOfDouble, длина и ширина которых равны 500, можно было бы сравнить их, преобразовав каждую в матрицу 500x500 и наблюдая, что все 250 000 значений совпадают, но если любой тип знает о другом, это может быть в состоянии сделать сравнение быстрее [например, диагональная матрица может знать, что она может соответствовать нулевой матрице тогда и только тогда, когда все элементы на диагонали равны нулю]. - person supercat; 12.11.2013
comment
Первая проблема, о которой вы говорите, не имеет ничего общего ни с Collection, ни даже с equals. Это реализация HashMap (используемая HashSet внутри), которая вызывает проблемы. HashSet разрывает контракт с Set. TreeSet делает то же самое, но, по крайней мере, это явно задокументировано. Последняя описанная вами проблема связана с тем, как equals реализован в базовом классе, а не с тем, какие поля вы используете. - person a better oliver; 10.03.2015
comment
Я бы сказал @zeroflagL, что это то же самое для всех Collection: List, Queue и Set, но хороший момент по поводу отсутствия Map - я добавил его. Если вы говорите list.contains(...), используется equals(...), и лучшим шаблоном является равенство идентификаторов. Что касается второго пункта, то на самом деле это поля, которые базовый класс выбрал для использования для обеспечения равенства. - person Gray; 10.03.2015
comment
Я согласен с вами насчет равенства тождеств (хотя в этом есть свои подводные камни), но в вашем примере проблема не в этом. Используйте другую реализацию Set, и все будет работать так, как ожидалось, независимо от того, совпадает ли идентификатор или нет. Проблема с Point в том, что он не заботится о подтипах. p.equals(c) должен вернуть false в первую очередь. В частности, если вы следуете идее равенства тождества: цветная точка — это не то же самое, что простая точка. Можно возразить, что Point должен либо быть final, либо учитывать подтипы в своем методе equals. - person a better oliver; 10.03.2015
comment
Любая реализация Set, которая использует equals(...), будет иметь проблему, если вы не определяете equals как равенство тождества, а это означает все реализации Set. Правильно? Я не совсем понимаю твою точку зрения @zeroflagL. - person Gray; 10.03.2015
comment
Нет. Проблема с HashSet заключается в том, что equals не вызывается в вашем примере. Если бы была вызвана equals, она вернула бы true, потому что foo1.equals(foo1) == true. Таким образом, вторая ссылка не будет добавлена ​​в набор. - person a better oliver; 10.03.2015
comment
О, я вижу @zeroflagL. Хорошая точка зрения. Я изменил его, чтобы убедиться, что это другой экземпляр объекта. - person Gray; 11.03.2015
comment
Ваша личность равна связи не работает. - person Basil Bourque; 21.09.2018
comment
К вашему сведению… Что касается того, что если идентификатор Foo является полем идентификатора, то второй fooSet.add не должен добавлять еще один элемент в набор, но должен заменить первый, поскольку они имеют один и тот же идентификатор., Set::add обещает обратное: Если этот набор уже содержит элемент, вызов оставляет набор без изменений и возвращает false. - person Basil Bourque; 22.09.2018
comment
Ух, вау. Не знаю, как я это пропустил. Фиксированный. Спасибо @BasilBourque - person Gray; 24.09.2018

Скопировано из Object.equals(Object obj) javadoc:

Указывает, является ли какой-либо другой объект «равным» этому.

Метод equals реализует отношение эквивалентности непустых ссылок на объекты:

  • Это рефлексивно: для любого ненулевого ссылочного значения x функция x.equals(x) должна возвращать true.
  • Он симметричен: для любых ненулевых ссылочных значений x и y функция x.equals(y) должна возвращать истину тогда и только тогда, когда y.equals(x) возвращает истину.
  • Это транзитивно: для любых ненулевых ссылочных значений x, y и z, если x.equals(y) возвращает true, а y.equals(z) возвращает true, то x.equals(z) должен возвращать true.
  • Он непротиворечив: для любых ненулевых ссылочных значений x и y множественные вызовы x.equals(y) последовательно возвращают true или последовательно возвращают false, при условии, что никакая информация, используемая в сравнениях на равенство для объектов, не изменяется.
  • Для любого ненулевого ссылочного значения x функция x.equals(null) должна возвращать false.

Это довольно ясно для меня, так должны работать равные. Что касается выбора полей, вы выбираете ту комбинацию полей, которая требуется для определения является ли какой-либо другой объект «равным» этому объекту.

Что касается вашего конкретного случая, если вам в вашем тесте нужна более широкая область равенства, вы реализуете это в своем тесте. Вы не должны взламывать свой метод equals только для того, чтобы он подходил.

person pap    schedule 16.03.2012
comment
Согласованный. Кроме того, я добавил псевдоправило здравого смысла в свой собственный ответ. - person Nicolas C.; 16.03.2012

Вы должны использовать все значимые переменные, т.е. переменные, значение которых не является производным от других, в равных.

Это из эффективной Java:

Для каждого «значимого» поля в классе проверьте, соответствует ли это поле аргумента соответствующему полю этого объекта.

Если вы хотите сопоставить идентификаторы, потому что это уникальный идентификатор для этого класса, просто сравните значение идентификатора, не используйте в этом случае равенство.

Если у вас есть уникальный идентификатор, язык не позволяет вам обеспечить, чтобы не было другого объекта с этим идентификатором или чтобы остальные значения переменных совпадали. Однако вы можете определить это в документации класса и использовать утверждения в реализации equals или в другом месте, поскольку это инвариант, заданный семантикой вашего класса.

person DPM    schedule 16.03.2012
comment
Если вы хотите сопоставить идентификаторы, потому что это уникальный идентификатор для этого класса, просто сравните значение идентификатора, не используйте в этом случае равенство. -- как насчет помещения экземпляров в Set или Map? Для этого мне нужны правильные равенства, и он сравнивает их по идентификатору. Как насчет этой ситуации? - person Andrey Balaguta; 17.03.2012
comment
@AndreyBalaguta: Если идентификатор действительно должен быть уникальным, нет необходимости переопределять Equals. Вместо этого используйте своего рода Map<Integer,YourType> для хранения ссылки на единственный YourType, который будет существовать для любого заданного идентификатора. - person supercat; 12.11.2013

Я бы не стал думать о модульном тесте при написании equals(), они разные.

Вы определяете равенство каждого объекта с одним или группой свойств, реализуя equals() и hashcode().

В вашем тесте, если вы хотите сравнить все свойства объекта, то, очевидно, вам нужно вызвать каждый метод.

Я думаю, что лучше рассматривать их отдельно.

person ManuPK    schedule 16.03.2012

Я думаю, что единственной лучшей практикой при переопределении метода equals() является здравый смысл.

Правил нет, кроме определение эквивалентности API Java. После того, как вы выбрали это определение равенства, вы также должны применить его к своему методу hashCode().

Под этим я подразумеваю, что как разработчик вы, ваши коллеги и сопровождающие должны знать, когда ваш экземпляр, скажем, Watermelon равен другому экземпляру объекта.

person Nicolas C.    schedule 16.03.2012