Apache Commons equals/hashCode builder
Мне интересно знать, что люди здесь думают об использовании org.apache.commons.lang.builder
EqualsBuilder
/HashCodeBuilder
для реализации equals
/hashCode
? Будет ли это лучше, чем писать свои собственные? Хорошо ли играет с Hibernate? Каково твое мнение?
8 ответов
Компоненты commons/lang великолепны, и я использую их годами без заметного снижения производительности (с и без спящего режима). Но, как пишет Ален, путь гуавы еще приятнее:
Вот образец Боба:
public class Bean{
private String name;
private int length;
private List<Bean> children;
}
Вот equals() и hashCode(), реализованные с помощью Commons/Lang:
@Override
public int hashCode(){
return new HashCodeBuilder()
.append(name)
.append(length)
.append(children)
.toHashCode();
}
@Override
public boolean equals(final Object obj){
if(obj instanceof Bean){
final Bean other = (Bean) obj;
return new EqualsBuilder()
.append(name, other.name)
.append(length, other.length)
.append(children, other.children)
.isEquals();
} else{
return false;
}
}
и здесь с Java 7 или выше (вдохновленный Guava):
@Override
public int hashCode(){
return Objects.hash(name, length, children);
}
@Override
public boolean equals(final Object obj){
if(obj instanceof Bean){
final Bean other = (Bean) obj;
return Objects.equals(name, other.name)
&& length == other.length // special handling for primitives
&& Objects.equals(children, other.children);
} else{
return false;
}
}
Примечание: этот код изначально ссылался на Guava, но, как отмечалось в комментариях, эта функциональность с тех пор была введена в JDK, поэтому Guava больше не требуется.
Как видите, версия Guava / JDK короче и избегает лишних вспомогательных объектов. В случае равных он даже позволяет закорачивать оценку, если ранее Object.equals()
call возвращает false (чтобы быть справедливым: commons / lang имеет ObjectUtils.equals(obj1, obj2)
метод с идентичной семантикой, который можно использовать вместо EqualsBuilder
разрешить короткое замыкание, как указано выше).
Итак: да, общие строители Lang очень предпочтительнее, чем построенные вручную equals()
а также hashCode()
методы (или эти ужасные монстры, которые Eclipse сгенерирует для вас), но версии Java 7+ / Guava еще лучше.
И заметка о Hibernate:
будьте осторожны с использованием отложенных коллекций в ваших реализациях equals(), hashCode() и toString(). Это с треском провалится, если у вас нет открытого сеанса.
Примечание (примерно равно ()):
a) в обеих версиях equals(), приведенных выше, вы можете использовать один или оба из этих ярлыков:
@Override
public boolean equals(final Object obj){
if(obj == this) return true; // test for reference equality
if(obj == null) return false; // test for null
// continue as above
б) в зависимости от вашей интерпретации контракта equals(), вы также можете изменить строку (и)
if(obj instanceof Bean){
в
// make sure you run a null check before this
if(obj.getClass() == getClass()){
Если вы используете вторую версию, вы, вероятно, также хотите позвонить super(equals())
внутри вашего equals()
метод. Здесь мнения расходятся, тема обсуждается в этом вопросе:
правильный способ включить суперкласс в реализацию Guava Objects.hashcode ()?
(хотя речь идет о hashCode()
то же самое относится и к equals()
)
Примечание (вдохновлено комментарием от kayahr)
Objects.hashCode(..)
(так же, как основной Arrays.hashCode(...)
) может работать плохо, если у вас много примитивных полей. В таких случаях, EqualsBuilder
на самом деле может быть лучшим решением.
Если вы не хотите зависеть от сторонней библиотеки (возможно, вы используете устройство с ограниченными ресурсами) и даже не хотите вводить свои собственные методы, вы также можете позволить IDE выполнять свою работу, например, при использовании Eclipse.
Source -> Generate hashCode() and equals()...
Вы получите "нативный" код, который вы можете настроить по своему усмотрению и который вы должны поддерживать при изменениях.
Пример (затмение Юнона):
import java.util.Arrays;
import java.util.List;
public class FooBar {
public String string;
public List<String> stringList;
public String[] stringArray;
/* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((string == null) ? 0 : string.hashCode());
result = prime * result + Arrays.hashCode(stringArray);
result = prime * result
+ ((stringList == null) ? 0 : stringList.hashCode());
return result;
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
FooBar other = (FooBar) obj;
if (string == null) {
if (other.string != null)
return false;
} else if (!string.equals(other.string))
return false;
if (!Arrays.equals(stringArray, other.stringArray))
return false;
if (stringList == null) {
if (other.stringList != null)
return false;
} else if (!stringList.equals(other.stringList))
return false;
return true;
}
}
EqualsBuilder и HashCodeBuilder имеют два основных аспекта, которые отличаются от написанного вручную кода:
- нулевая обработка
- создание экземпляра
EqualsBuilder и HashCodeBuilder упрощают сравнение полей, которые могут быть нулевыми. С помощью написанного вручную кода это создает много шаблонов.
EqualsBuilder, с другой стороны, создаст экземпляр для вызова метода equals. Если ваши методы equals вызываются часто, это создаст много экземпляров.
Для Hibernate реализация equals и hashCode не имеет значения. Они просто деталь реализации. Почти для всех объектов домена, загруженных с помощью hibernate, издержки времени исполнения (даже без анализа escape) Builder можно игнорировать. Затраты на базу данных и связь будут значительными.
Как отметил Скаффман, версия отражения не может быть использована в рабочем коде. Отражение будет медленным, и "реализация" будет правильной не для всех, кроме простейших классов. Учет всех членов также опасен, так как вновь представленные члены меняют поведение метода equals. Версия отражения может быть полезна в тестовом коде.
Если вы не пишете свой собственный, есть также возможность использовать Google Guava (ранее коллекции Google)
На мой взгляд, это плохо сочетается с Hibernate, особенно в примерах из ответа, сравнивающих длину, имя и дочерние элементы для некоторой сущности. Hibernate рекомендует использовать бизнес-ключ для использования в equals() и hashCode(), и у них есть свои причины. Если вы используете генератор auto equals() и hashCode () для своего бизнес-ключа, это нормально, нужно учитывать только проблемы с производительностью, как упоминалось ранее. Но люди обычно используют все свойства, что IMO очень неправильно. Например, в настоящее время я работаю над проектом, в котором сущности пишутся с использованием Pojomatic с @AutoProperty, что я считаю очень плохим паттерном.
Их два основных сценария использования hashCode () и equals():
- когда вы помещаете экземпляры постоянных классов в набор (рекомендуемый способ представления многозначных ассоциаций) и
- когда вы используете присоединение отдельных экземпляров
Итак, давайте предположим, что наша сущность выглядит так:
class Entity {
protected Long id;
protected String someProp;
public Entity(Long id, String someProp);
}
Entity entity1 = new Entity(1, "a");
Entity entity2 = new Entity(1, "b");
Оба являются одним и тем же объектом для Hibernate, который был извлечен из некоторого сеанса в какой-то момент (их id и класс / таблица равны). Но когда мы реализуем auto equals() hashCode () на всех объектах, что мы имеем?
- Когда вы помещаете entity2 в постоянный набор, где entity1 уже существует, он будет помещен дважды и приведет к исключению во время фиксации.
- Если вы хотите присоединить отделенную сущность 2 к сеансу, где сущность 1 уже существует, они (вероятно, я не проверял это особенно) не будут объединены должным образом.
Итак, для 99% проекта, который я делаю, мы используем следующую реализацию equals() и hashCode(), написанную один раз в базовом классе сущности, что согласуется с концепциями Hibernate:
@Override
public boolean equals(Object obj) {
if (StringUtils.isEmpty(id))
return super.equals(obj);
return getClass().isInstance(obj) && id.equals(((IDomain) obj).getId());
}
@Override
public int hashCode() {
return StringUtils.isEmpty(id)
? super.hashCode()
: String.format("%s/%s", getClass().getSimpleName(), getId()).hashCode();
}
Для временной сущности я делаю то же самое, что и Hibernate на шаге персистентности, т.е. Я использую экземпляр матча. Для постоянных объектов я сравниваю уникальный ключ, который является таблицей / идентификатором (я никогда не использую составные ключи).
Если вы просто имеете дело с бином сущности, где id является первичным ключом, вы можете упростить.
@Override
public boolean equals(Object other)
{
if (this == other) { return true; }
if ((other == null) || (other.getClass() != this.getClass())) { return false; }
EntityBean castOther = (EntityBean) other;
return new EqualsBuilder().append(this.getId(), castOther.getId()).isEquals();
}
На всякий случай, другие сочтут это полезным, я придумал этот класс Helper для вычисления хеш-кода, который позволяет избежать дополнительных затрат на создание объекта, упомянутых выше (на самом деле, издержки метода Objects.hash() еще больше, когда у вас есть наследование, поскольку это создаст новый массив на каждом уровне!).
Пример использования:
public int hashCode() {
return HashCode.hash(HashCode.hash(timestampMillis), name, dateOfBirth); // timestampMillis is long
}
public int hashCode() {
return HashCode.hash(super.hashCode(), occupation, children);
}
Помощник HashCode:
public class HashCode {
public static int hash(Object o1, Object o2) {
return add(Objects.hashCode(o1), o2);
}
public static int hash(Object o1, Object o2, Object o3) {
return hash(Objects.hashCode(o1), o2, o3);
}
...
public static int hash(Object o1, Object o2, ..., Object o10) {
return hash(Objects.hashCode(o1), o2, o3, ..., o10);
}
public static int hash(int initial, Object o1, Object o2) {
return add(add(initial, o1), o2);
}
...
public static int hash(int initial, Object o1, Object o2, ... Object o10) {
return add(... add(add(add(initial, o1), o2), o3) ..., o10);
}
public static int hash(long value) {
return (int) (value ^ (value >>> 32));
}
public static int hash(int initial, long value) {
return add(initial, hash(value));
}
private static int add(int accumulator, Object o) {
return 31 * accumulator + Objects.hashCode(o);
}
}
Я полагал, что 10 - это максимально разумное количество свойств в доменной модели, если у вас есть больше, вы должны подумать о рефакторинге и введении большего количества классов вместо того, чтобы поддерживать кучу строк и примитивов.
Недостатки: бесполезно, если у вас есть в основном примитивы и / или массивы, которые нужно глубоко хэшировать. (Обычно это тот случай, когда вам приходится иметь дело с плоскими (переносными) объектами, которые находятся вне вашего контроля).