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

StackOverflow https://stackoverflow.com/questions/27581

Вопрос

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

Это было полезно?

Решение

Теория (для лингвистов и тех, кто склонен к математике):

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

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

Тот Самый отношение между этими двумя методами есть:

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

На практике:

Если вы переопределяете одно, то вам следует переопределить и другое.

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

Используйте отличные вспомогательные классы Эквалсбилдер и Конструктор хэшкодов из самого Общий Язык Apache библиотека.Пример:

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();
    }
}

Также помните:

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

Другие советы

Есть некоторые проблемы, на которые стоит обратить внимание, если вы имеете дело с классами, которые сохраняются с использованием средства сопоставления объектных отношений (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 это единственное, что будет вести себя правильно.

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

ORM обычно используют геттеры для принудительной загрузки лениво загруженных объектов.Это означает , что 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() plus 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)) результат выдает true, как и должно быть.

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

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". Тал не согласен.

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

Пример:

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() должна работать во всех иерархиях наследования, если Принцип замещения Лискова это значит быть удовлетворенным.

Все еще удивлен, что никто не порекомендовал библиотеку guava для этого.

 //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.lang.Object.Нам нужно переопределить их для пользовательского объекта.

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;
  }

}

Для получения равных, загляните в Секреты равных Автор: Angelika Langer.Мне это очень нравится.Она также отличный часто задаваемый вопрос о Дженерики на 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()

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top