Java: чистый способ автоматического броска UnsupportedOperationException при вызове hashCode() и equals()?

У нас есть OO-кодовая база, где во многих случаях hashcode() и equals() просто не работают, главным образом по следующей причине:

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

Вот цитата из "Эффективной Явы" Джошуа Блоха и там больше на эту тему в большой статье Артимы:

http://www.artima.com/lejava/articles/equality.html

И мы отлично справляемся с этим, это не тот вопрос, о котором идет речь.

Возникает вопрос: видно ли, что в каком-то случае вы не можете удовлетворить контракт equals(), какой бы чистый способ автоматически сделать hashcode() и equals() бросить исключение UnsupportedOperationException?

Будет ли аннотация работать? Я думаю о чем-то вроде @NotNull: каждое нарушение контракта @NotNull делает автоматическое исключение, и вам больше нечего делать, кроме аннотирования ваших параметров/возвращаемого значения с помощью @NotNull.

Это удобно, потому что это 8 символов ( "@NotNull" ) вместо постоянного повторения одного и того же кода проверки/исключения исключений.

В случае, о котором я беспокоюсь, в каждой реализации, где hashCode()/equals() не имеет смысла, мы всегда повторяем одно и то же:

@Override
public int hashCode() {
    throw new UnsupportedOperationException( "contract violation: calling hashCode() on such an object makes no sense" );
}

@Override
public boolean equals( Object o ) {
    throw new UnsupportedOperationException( "contract violation: calling equals() on such an object makes no sense" );
}

Однако это ошибка: мы можем по ошибке забыть вырезать/вставить это, и это может привести к неправильному использованию пользователями таких объектов (скажем, пытаясь поместить их в коллекции Java по умолчанию).

Или если аннотации не могут быть созданы для создания такого поведения, будет ли AOP работать?

Интересно, что реальная проблема заключается в самом присутствии hashcode() и equals() в верхней части иерархии Java, которая просто не имеет смысла в некоторых случаях. Но тогда как мы справляемся с этой проблемой чисто?

Ответы

Ответ 1

Почему бы вам не позволить вашей среде IDE (Eclipse/NetBeans/IntelliJ) генерировать методы hashCode() и equals() для вас. Они неплохо справляются с этим.

AOP будет работать, конечно, но это довольно сложное явление. И это будет означать, что вы не сможете использовать эти объекты почти без сбора или полезности.

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

Ответ 2

Я согласен с вашей оценкой того, что это проблема с hashCode и equals, определяемая в Object в первую очередь. Я давно придерживался мнения о том, что равенство должно обрабатываться так же, как и при заказе - с интерфейсом, говорящим "Я могу сравниться с экземпляром X", а другое высказывание "Я могу сравнить два экземпляра X".

С другой стороны, это действительно вызвало ошибки? Попробовали ли люди использовать equals и hashCode, где они не должны? Потому что даже если вы можете сделать каждый класс в вашей кодовой базе, вызывать исключение, когда эти методы называются ненадлежащим образом, это не относится к другим классам, которые вы используете, будь то из JDK или сторонних библиотек.

Я уверен, что вы могли бы сделать это с AOP той или иной формы, будь то обычная обработка аннотации или что-то еще, - но есть ли у вас доказательства того, что вознаграждение будет стоить усилий?

Другой способ взглянуть на это: это только в том случае, если вы расширяете другой класс, который уже переопределяет hashCode и equals, правильно? В противном случае вы можете использовать принцип "равенство = идентичность" методов Object hashCode/equals, которые могут быть полезны. У вас очень много классов, которые попадают в эту категорию? Не могли бы вы просто написать unit test, чтобы найти все такие типы через отражение, и проверить, что эти типы вызывают исключение, когда вы вызываете hashCode/equals? (Это может быть автоматизировано, если у них есть конструктор без параметров или имеется ручной список типов, которые были проверены, - unit test может завершиться неудачей, если есть новый тип, который не находится в списке "известных хороших".)

Ответ 3

Я не понимаю, почему вы думаете, что "в каком-то случае вы не можете удовлетворить контракт equals()"? Значение равенства определяется классом. Таким образом, использование объектов, равных, вполне допустимо. Если вы не переопределяете равны, то вы определяете каждый экземпляр как уникальный.

Кажется, существует неправильное представление о том, что equals - один из тех методов, которые всегда требуют переопределения и что он должен проверять все его поля. Я бы утверждал, что обратное - не переопределять равные, если ваше определение равенства не отличается.

Я также не согласен с статьей artima, в частности "Pitfall № 3: Определение равных в терминах изменяемых полей". Его совершенно справедливо для класса, чтобы определить его равенство, основанное на изменяемых полях. Его пользователь должен знать об этом при использовании его с коллекциями. Если изменяемый объект определяет свое равенство в своем изменяемом состоянии, то не ожидайте, что два экземпляра будут равны после того, как он изменился.

Я думаю, что бросание UnsupportedOperation нарушает спринт равных. Объекты равны состояниям:

Метод equals для класса Object реализует наиболее дискриминационные возможное отношение эквивалентности на объекты; то есть для любого непустого опорные значения x и y, этот метод возвращает true тогда и только тогда, когда x и y обратитесь к одному и тому же объекту (x == y имеет значение true).

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

Ответ 4

Существует не менее двух отношений эквивалентности, которые могут быть определены между всеми объектами в Java или .NET:

  • Две ссылки на объекты X и Y полностью эквивалентны, если переписывание X со ссылкой на Y не повлияет на существующее или будущее поведение любых членов X или Y.

  • Две ссылки на объекты X и Y имеют эквивалентное состояние, если в программе, которая не сохраняла значения, возвращаемые из хэш-функции, связанной с идентификацией, замена всех ссылок на X со всеми ссылками на Y оставила неизменным состояние программы.

У меня есть одна ссылка (X), которая указывает на FordBlazer. У меня есть другой (Y), который указывает на SiameseCat. Они эквивалентны? Нет, это не так, поэтому X.equals(Y) должен быть ложным. Тот факт, что типы объектов не имеют отношения друг к другу, не является проблемой - во всяком случае, это упрощает (если единственное, что может быть эквивалентно сиамской кате, это еще один сиамский котик, тот факт, что Y isn 't a SiameseCat означает, что X.equals() не нужно ничего изучать.

Хотя могут быть некоторые споры о том, должен ли конкретный объект реализовать первое или второе определение эквивалентности, стоит отметить, что любой объект, который определяет equals, сообщает о различных объектах как неравные независимо от каких-либо других аспектов их состояния, будет согласуется с самим собой (если X.Equals(X) не соответствует X.Equals(Y), это означает, что Y не ведет себя так же, как X). Таким образом, если с equals нет ничего лучше, определение по умолчанию, унаследованное от object, является совершенно хорошим и значимым.

Единственная ситуация, при которой hashCode может возникнуть проблема, будет заключаться в том, что код может (недобросовестно) мутировать какой-либо аспект объекта, пока он хранится в HashTable. правильное средство для этого состоит в том, чтобы hashCode не зависело от каких-либо изменяемых аспектов состояния объекта. Если состояние объекта не имеет значимых неизменных аспектов, отличных от его класса, просто составляйте произвольное число для этого класса и hashCode всегда возвращает это. Большие хэш-таблицы будут плохо работать с такими объектами, но небольшие хеш-коды будут работать нормально. Тот факт, что нельзя определить хороший хэш-код для типа, не должен препятствовать его использованию в HashTable с дюжиной или около того элементов в нем.