Apache Commons equals / hashCode builder [закрыто]

155

Мне любопытно узнать, что люди здесь думают об использовании org.apache.commons.lang.builder EqualsBuilder/ HashCodeBuilder для реализации equals/ hashCode? Будет ли это лучше, чем писать свои собственные? Хорошо ли играет с Hibernate? Каково ваше мнение?

aug70co
источник
16
Только не соблазниться reflectionEqualsи reflectionHashcodeфункций; производительность является абсолютным убийцей.
Скаффман
14
Вчера я видел дискуссию о равных, и у меня было немного свободного времени, поэтому я сделал быстрый тест. У меня было 4 объекта с разными реализациями equals. сгенерированный eclipse, equalsbuilder.append, equalsbuilder.reflection и pojomatic аннотации. Базовая линия была затмением. equalsbuilder.append занял 3,7 раза. pojomatic взял 5x. отражение на основе заняло 25,8х. Это было довольно обескураживающим, потому что мне нравится простота основанного на отражении, и я не выношу название "pojomatic".
digitaljoel
5
Другой вариант - проект Ломбок; он использует генерацию байт-кода, а не отражение, поэтому он должен работать так же, как Eclipse-генерируемый. projectlombok.org/features/EqualsAndHashCode.html
Миль

Ответы:

212

Компоненты 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 или выше (вдохновленный Гуавой):

@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()вызов возвращает false (справедливо: у commons / lang есть ObjectUtils.equals(obj1, obj2)метод с идентичной семантикой, который можно использовать вместо того, EqualsBuilderчтобы разрешить короткое замыкание, как указано выше).

Итак: да, сборщики общего языка более предпочтительны, чем созданные вручную equals()и hashCode()методы (или те ужасные монстры, которые Eclipse сгенерирует для вас), но версии Java 7+ / Guava еще лучше.

И заметка о Hibernate:

Будьте осторожны с использованием отложенных коллекций в ваших реализациях equals (), hashCode () и toString (). Это с треском провалится, если у вас нет открытого сеанса.


Примечание (примерно равно ()):

а) в обеих версиях 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самом деле , может быть лучшим решением.

Шон Патрик Флойд
источник
34
То же самое будет возможно с Java 7 Objects.equals: download.oracle.com/javase/7/docs/api/java/util/…
Томас Юнг
3
Если я правильно читаю, Джош Блох говорит в Effective Java , Item 8, что вам не следует использовать getClass () в вашем методе equals (); скорее вы должны использовать instanceof.
Джефф Олсон
6
@SeanPatrickFloyd Guava-путь не только создает объект массива для varargs, он также преобразует ВСЕ параметры в объекты. Поэтому, когда вы передадите ему 10 значений int, вы получите 10 объектов Integer и объект массива. Решение commons-lang создает только один объект, независимо от того, сколько значений вы добавляете в хеш-код. Та же проблема с equals. Guava преобразует все значения в объекты, commons-lang создает только один новый объект.
Каяр
1
@wonhee Я категорически не согласен, что это лучше. Использование Reflection для вычисления хеш-кодов - это не то, что я когда-либо делал бы. Снижение производительности, вероятно, незначительно, но оно кажется неправильным.
Шон Патрик Флойд,
1
@kaushik, делая финал класса, фактически решает потенциальные проблемы обеих версий (instanceof и getClass ()), до тех пор, пока вы реализуете equals () только в листовых классах
Шон Патрик Флойд
18

Люди, проснись! Начиная с Java 7 в стандартной библиотеке есть вспомогательные методы для equals и hashCode . Их использование полностью эквивалентно использованию методов Guava.

Михаил Голубцов
источник
а) в момент, когда был задан этот вопрос, Java 7 еще не было б) технически они не совсем эквивалентны. jdk имеет метод Objects.equals по сравнению с методами Guava Objects.equal. Я могу использовать статический импорт только с версией Guava. Это просто косметика, я знаю, но это делает не-гуаву заметно более загроможденной.
Шон Патрик Флойд
Это не очень хороший метод для переопределения метода equals объекта из-за того, что Objects.equals будет вызывать метод экземпляра .equals. Если вы вызовите Objects.equals в методе экземпляра .equals, это приведет к переполнению стека.
Dardo
Можете ли вы привести пример, когда он попадает в цикл?
Михаил Голубцов
OP запрашивает переопределение метода equals () внутри объекта. Согласно документации статического метода Objects.equals (): «Возвращает true, если аргументы равны друг другу, и false в противном случае. Следовательно, если оба аргумента равны null, возвращается true, и если ровно один аргумент равен null, false равен вернулся. в противном случае, равенство определяется с помощью метода первого аргумента равно. "Поэтому, если вы использовали Objects.equals () в перекрытом экземпляре Equals () было бы назвать это методы собственных равно, то Objects.equals () затем снова сам, давая переполнение стека.
Dardo
@dardo Мы говорим о реализации структурного равенства, поэтому это означает, что два объекта равны друг другу, если их поля делают. Посмотрите пример Guava выше, как реализован метод equals.
Михаил Голубцов
8

Если вы не хотите зависеть от сторонней библиотеки (возможно, вы используете устройство с ограниченными ресурсами) и даже не хотите вводить свои собственные методы, вы также можете позволить среде 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;
    }

}
FrVaBe
источник
14
Да, но код, сгенерированный Eclipse, не читается и не поддерживается.
Шон Патрик Флойд
6
Пожалуйста, никогда не думайте о чем-то столь ужасном, как сгенерированное затмение equals. Если вы не хотите зависеть от сторонней библиотеки, напишите такой же однострочный метод, как и Objects.equalвы. Даже если он используется только один или два раза, он делает код лучше!
Maaartinus
@maaartinus equals/ hashCodeоднострочные методы ???
FrVaBe
1
@maaartinus Guava - сторонняя библиотека. Я указал, что мое решение может быть использовано, если вы хотите избежать использования сторонних библиотек.
FrVaBe
1
@FrVaBe: И я написал: «Если вы не хотите зависеть от сторонней библиотеки, напишите однострочный метод, например Objects.equal, самостоятельно». А потом я написал однострочный метод, который вы можете использовать, чтобы ИЗБЕГАТЬ, используя Гуаву, и все равно сократить длину примерно до половины.
Maaartinus
6

EqualsBuilder и HashCodeBuilder имеют два основных аспекта, которые отличаются от написанного вручную кода:

  • нулевая обработка
  • создание экземпляра

EqualsBuilder и HashCodeBuilder упрощают сравнение полей, которые могут быть нулевыми. С помощью написанного вручную кода это создает много шаблонов.

EqualsBuilder, с другой стороны, создаст экземпляр для вызова метода equals. Если ваши методы equals вызывают часто, это создаст много экземпляров.

Для Hibernate реализация equals и hashCode не имеет значения. Они просто деталь реализации. Почти для всех доменных объектов, загруженных с помощью hibernate, издержки времени исполнения (даже без анализа escape) Builder можно игнорировать . Затраты на базу данных и связь будут значительными.

Как отметил Скаффман, версия отражения не может быть использована в рабочем коде. Отражение будет медленным, и «реализация» будет правильной не для всех, кроме самых простых классов. Учет всех членов также опасен, так как новые участники изменяют поведение метода equals. Версия отражения может быть полезна в тестовом коде.

Томас Юнг
источник
Я не согласен с тем, что реализация рефлексии «не будет правильной для всех, кроме самых простых классов». Со строителями вы можете явно исключить поля, если хотите, поэтому реализация действительно зависит от определения вашего бизнес-ключа. К сожалению, я не могу не согласиться с аспектом производительности реализации, основанной на отражении.
digitaljoel
1
@digitaljoel Да, вы можете исключить поля, но эти определения не являются рефакторингом сохранения. Поэтому я не упомянул их специально.
Томас Юнг
0

Если вы просто имеете дело с бином сущности, где 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();
   }
ДЕРЕК ЛИ
источник
0

На мой взгляд, это плохо сочетается с 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 () на всех объектах, что мы имеем?

  1. Когда вы помещаете entity2 в постоянный набор, где entity1 уже существует, это будет помещено дважды и приведет к исключению во время фиксации.
  2. Если вы хотите присоединить отделенную сущность 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 на шаге персистентности, т.е. Я использую экземпляр матча. Для постоянных объектов я сравниваю уникальный ключ, который является таблицей / идентификатором (я никогда не использую составные ключи).

Лукаш Франковски
источник
0

На всякий случай, другие сочтут это полезным, я придумал этот класс 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 - это максимально разумное количество свойств в доменной модели, если у вас есть больше, вы должны подумать о рефакторинге и введении большего количества классов, вместо того, чтобы поддерживать кучу строк и примитивов.

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

Влад
источник