Какие проблемы следует учитывать при переопределении equals и hashCode в Java?


какие проблемы / подводные камни необходимо учитывать при переопределении equals и hashCode?

11 617

11 ответов:

теория (для юристов языка и математически склонных):

equals() (документация) необходимо определить отношение эквивалентности (оно должно быть рефлексивные,симметричного и транзитивное). Кроме того, он должен быть последовательный (если объекты не изменяются, то он должен продолжать возвращать то же значение). Кроме того, o.equals(null) должен всегда возвращать false.

hashCode() (документация) должно быть последовательный (если объект не был изменен в плане equals(), он должен продолжать возвращать то же значение).

The отношения между двумя методами:

всякий раз, когда a.equals(b), потом a.hashCode() должно быть таким же, как b.hashCode().

на практике:

если вы переопределяете один, то вы должны переопределить другой.

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

используйте отличные вспомогательные классы EqualsBuilder и HashCodeBuilder С Apache Commons Lang библиотека. Пример:

public class Person {
    private String name;
    private int age;
    // ...

    @Override
    public int hashCode() {
        return new HashCodeBuilder(17, 31). // two randomly chosen prime numbers
            // if deriving: appendSuper(super.hashCode()).
            append(name).
            append(age).
            toHashCode();
    }

    @Override
    public boolean equals(Object obj) {
       if (!(obj instanceof Person))
            return false;
        if (obj == this)
            return true;

        Person rhs = (Person) obj;
        return new EqualsBuilder().
            // if deriving: appendSuper(super.equals(obj)).
            append(name, rhs.name).
            append(age, rhs.age).
            isEquals();
    }
}

и помните:

при использовании хэша на основе коллекция или карта например HashSet,LinkedHashSet,HashMap, Hashtable или WeakHashMap, убедитесь, что хэш-код() ключевых объектов, которые вы помещаете в коллекцию, никогда не изменяется, пока объект находится в коллекции. Пуленепробиваемый способ убедиться в этом - сделать ваши ключи неизменными,который имеет и другие преимущества.

есть некоторые проблемы, которые стоит заметить, если вы имеете дело с классами, которые сохраняются с помощью Object-Relationship Mapper (ORM), как Hibernate, если вы не думали, что это было неоправданно сложно уже!

ленивые загруженные объекты являются подклассами

если ваши объекты сохраняются с помощью ORM, во многих случаях вы будете иметь дело с динамическими прокси, чтобы избежать загрузки объекта слишком рано из хранилища данных. Эти прокси реализованы в виде подклассы вашего собственного класса. Это значит, чтоthis.getClass() == o.getClass() вернутся false. Например:

Person saved = new Person("John Doe");
Long key = dao.save(saved);
dao.flush();
Person retrieved = dao.retrieve(key);
saved.getClass().equals(retrieved.getClass()); // Will return false if Person is loaded lazy

если вы имеете дело с ORM, используя o instanceof Person - это единственное, что будет вести себя правильно.

ленивые загруженные объекты имеют нулевые поля

ORMs обычно используют геттеры для принудительной загрузки ленивых загруженных объектов. Это значит, что person.name будет null если person лениво загружается, даже если person.getName() погрузка силами и возвращает "Джон Доу". По моему опыту, это появляется чаще в hashCode() и equals().

если вы имеете дело с ORM, убедитесь, что всегда использовать геттеры, а не ссылки на поля в hashCode() и equals().

сохранение объекта изменит его состояние

постоянные объекты часто использовать id поле для хранения ключа объекта. Это поле будет автоматически обновляться при первом сохранении объекта. Не используйте поле id в hashCode(). Но вы можете использовать его в equals().

шаблон, который я часто использую

if (this.getId() == null) {
    return this == other;
}
else {
    return this.getId().equals(other.getId());
}

но: вы не можете включить getId() на hashCode(). Если вы это сделаете, когда объект сохраняется, его hashCode изменения. Если объект находится в HashSet, вы "никогда" не найдете его снова.

в своем Person пример, я, вероятно, будет использовать getName() на hashCode и getId() плюс getName() (только для паранойи) для equals(). Это нормально, если есть некоторый риск "столкновения" для hashCode(), но не хорошо для equals().

hashCode() следует использовать неизменяемое подмножество свойств из equals()

уточнение по поводу obj.getClass() != getClass().

это утверждение является результатом equals() будучи наследование недружелюбно. JLS (спецификация языка Java) указывает, что если A.equals(B) == true затем B.equals(A) также должен вернуть true. Если вы опустите этот оператор, наследующий классы, которые переопределяют equals() (и изменить его поведение) нарушит эту спецификацию.

рассмотрим следующий пример того, что происходит, когда заявление опущено:

    class A {
      int field1;

      A(int field1) {
        this.field1 = field1;
      }

      public boolean equals(Object other) {
        return (other != null && other instanceof A && ((A) other).field1 == field1);
      }
    }

    class B extends A {
        int field2;

        B(int field1, int field2) {
            super(field1);
            this.field2 = field2;
        }

        public boolean equals(Object other) {
            return (other != null && other instanceof B && ((B)other).field2 == field2 && super.equals(other));
        }
    }    

делаешь new A(1).equals(new A(1)) и new B(1,1).equals(new B(1,1)) результат выдают истинный, как и положено.

это выглядит все очень хорошо, но посмотрите, что произойдет, если мы попытаемся использовать оба класса:

A a = new A(1);
B b = new B(1,1);
a.equals(b) == true;
b.equals(a) == false;

очевидно, что это не так.

если вы хотите обеспечить симметричное состояние. a=b, если b=a и принцип подстановки Лискова вызывают super.equals(other) не только в случае B экземпляр, но проверьте после A пример:

if (other instanceof B )
   return (other != null && ((B)other).field2 == field2 && super.equals(other)); 
if (other instanceof A) return super.equals(other); 
   else return false;

что будет на выходе:

a.equals(b) == true;
b.equals(a) == true;

где, если a не является ссылкой на B, то это может быть ссылка класса A (потому что вы расширяете его), в этом случае вы называете super.equals()слишком.

для реализации, удобной для наследования, проверьте решение Тала Коэна,Как правильно реализовать метод equals ()?

резюме:

в своей книге Эффективное Руководство По Языку Программирования Java (Addison-Wesley, 2001), Джошуа блох утверждает, что "просто нет способа расширить экземпляр класса и добавить аспект, сохраняя контракт equals.- Тал не согласен.

его решение заключается в реализации равных() вызывая другой несимметричный blindlyEquals () в обоих направлениях. blindlyEquals() переопределяется подклассами, равна() передается по наследству, и никогда не переопределяется.

пример:

class Point {
    private int x;
    private int y;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof Point))
            return false;
        Point p = (Point)o;
        return (p.x == this.x && p.y == this.y);
    }
    public boolean equals(Object o) {
        return (this.blindlyEquals(o) && o.blindlyEquals(this));
    }
}

class ColorPoint extends Point {
    private Color c;
    protected boolean blindlyEquals(Object o) {
        if (!(o instanceof ColorPoint))
            return false;
        ColorPoint cp = (ColorPoint)o;
        return (super.blindlyEquals(cp) && 
        cp.color == this.color);
    }
}

обратите внимание, что equals () должен работать через иерархии наследования, если Принцип Подстановки Лискова подлежит удовлетворению.

все еще удивляюсь, что никто не рекомендовал библиотеку гуавы для этого.

 //Sample taken from a current working project of mine just to illustrate the idea

    @Override
    public int hashCode(){
        return Objects.hashCode(this.getDate(), this.datePattern);
    }

    @Override
    public boolean equals(Object obj){
        if ( ! obj instanceof DateAndPattern ) {
            return false;
        }
        return Objects.equal(((DateAndPattern)obj).getDate(), this.getDate())
                && Objects.equal(((DateAndPattern)obj).getDate(), this.getDatePattern());
    }

есть два метода в суперклассе как java.ленг.Объект. Нам нужно переопределить их в пользовательский объект.

public boolean equals(Object obj)
public int hashCode()

равные объекты должны производить один и тот же хэш-код, пока они равны, однако неравные объекты не должны производить различные хэш-коды.

public class Test
{
    private int num;
    private String data;
    public boolean equals(Object obj)
    {
        if(this == obj)
            return true;
        if((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        // object must be Test at this point
        Test test = (Test)obj;
        return num == test.num &&
        (data == test.data || (data != null && data.equals(test.data)));
    }

    public int hashCode()
    {
        int hash = 7;
        hash = 31 * hash + num;
        hash = 31 * hash + (null == data ? 0 : data.hashCode());
        return hash;
    }

    // other methods
}

Если вы хотите получить больше, пожалуйста, проверьте эту ссылку как http://www.javaranch.com/journal/2002/10/equalhash.html

это другой образец, http://java67.blogspot.com/2013/04/example-of-overriding-equals-hashcode-compareTo-java-method.html

Удачи! @.@

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

  1. использовать instanceof оператора.
  2. использовать this.getClass().equals(that.getClass()).

я использую #1 в final реализация equals, или при реализации интерфейса, который предписывает алгоритм для equals (например,java.util интерфейсы сбора-правильный способ проверить с помощью (obj instanceof Set) или что интерфейс, который вы реализуете). Обычно это плохой выбор, когда равные могут быть переопределены, потому что это нарушает свойство симметрии.

опция #2 позволяет безопасно расширить класс без переопределения equals или нарушения симметрии.

если ваш класс тоже Comparable на equals и compareTo методы должны быть последовательными. Вот шаблон для метода Equals в Comparable класс:

final class MyClass implements Comparable<MyClass>
{

  …

  @Override
  public boolean equals(Object obj)
  {
    /* If compareTo and equals aren't final, we should check with getClass instead. */
    if (!(obj instanceof MyClass)) 
      return false;
    return compareTo((MyClass) obj) == 0;
  }

}

для равных, посмотрите в секреты равна by Анжелика Лангер. Мне это очень нравится. Она также большой FAQ о дженерики в Java. Посмотреть ее другие статьи здесь (прокрутите вниз до" Core Java"), где она также продолжает Часть 2 и"сравнение смешанных типов". Получайте удовольствие, читая их!

метод Equals() используется для определения равенства двух объектов.

как int значение 10 всегда равно 10. Но этот метод equals () - это равенство двух объектов. Когда мы говорим объект, он будет иметь свойства. Для принятия решения о равенстве рассматриваются эти свойства. Нет необходимости, чтобы все свойства должны быть приняты во внимание для определения равенства и в отношении определения класса и контекста он может быть решен. Тогда метод equals () может быть переопределенный.

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

данная реализация по умолчанию метода hashCode() в классе Object использует внутренний адрес объекта и преобразует его в целое и вернуть его.

public class Tiger {
  private String color;
  private String stripePattern;
  private int height;

  @Override
  public boolean equals(Object object) {
    boolean result = false;
    if (object == null || object.getClass() != getClass()) {
      result = false;
    } else {
      Tiger tiger = (Tiger) object;
      if (this.color == tiger.getColor()
          && this.stripePattern == tiger.getStripePattern()) {
        result = true;
      }
    }
    return result;
  }

  // just omitted null checks
  @Override
  public int hashCode() {
    int hash = 3;
    hash = 7 * hash + this.color.hashCode();
    hash = 7 * hash + this.stripePattern.hashCode();
    return hash;
  }

  public static void main(String args[]) {
    Tiger bengalTiger1 = new Tiger("Yellow", "Dense", 3);
    Tiger bengalTiger2 = new Tiger("Yellow", "Dense", 2);
    Tiger siberianTiger = new Tiger("White", "Sparse", 4);
    System.out.println("bengalTiger1 and bengalTiger2: "
        + bengalTiger1.equals(bengalTiger2));
    System.out.println("bengalTiger1 and siberianTiger: "
        + bengalTiger1.equals(siberianTiger));

    System.out.println("bengalTiger1 hashCode: " + bengalTiger1.hashCode());
    System.out.println("bengalTiger2 hashCode: " + bengalTiger2.hashCode());
    System.out.println("siberianTiger hashCode: "
        + siberianTiger.hashCode());
  }

  public String getColor() {
    return color;
  }

  public String getStripePattern() {
    return stripePattern;
  }

  public Tiger(String color, String stripePattern, int height) {
    this.color = color;
    this.stripePattern = stripePattern;
    this.height = height;

  }
}

Пример Вывода Кода:

bengalTiger1 and bengalTiger2: true 
bengalTiger1 and siberianTiger: false 
bengalTiger1 hashCode: 1398212510 
bengalTiger2 hashCode: 1398212510 
siberianTiger hashCode: –1227465966

логически мы имеем:

a.getClass().equals(b.getClass()) && a.equals(b)a.hashCode() == b.hashCode()

но не наоборот!

один gotcha я нашел, где два объекта содержат ссылки друг на друга (один пример является родительским/дочерним отношением с методом удобства на родителе, чтобы получить всех детей).
Такие вещи довольно распространены, например, при выполнении сопоставлений Hibernate.

Если вы включаете оба конца отношения в свой хэш-код или тесты equals, можно попасть в рекурсивный цикл, который заканчивается в StackOverflowException.
Самое простое решение чтобы не включать коллекцию getChildren в методы.