Каков правильный способ переопределения методов hashCode() и equals() для постоянной сущности?

У меня есть простая роль класса:

@Entity
@Table (name = "ROLE")
public class Role implements Serializable {

    @Id
    @GeneratedValue
    private Integer id;
    @Column
    private String roleName;

    public Role () { }

    public Role (String roleName) {
        this.roleName = roleName;
    }

    public void setId (Integer id) {
        this.id = id;
    }

    public Integer getId () {
        return id;
    }

    public void setRoleName (String roleName) {
        this.roleName = roleName;
    }

    public String getRoleName () {
        return roleName;
    }
}

Теперь я хочу переопределить его методы equals и hashCode. Мое первое предложение:

public boolean equals (Object obj) {
    if (obj instanceof Role) {
        return ((Role)obj).getRoleName ().equals (roleName);
    }
    return false;
}

public int hashCode () {
    return id; 
}

Но когда я создаю новый объект Role, его id равно null. Вот почему у меня есть некоторые проблемы с реализацией метода hashCode. Теперь я могу просто вернуть roleName.hashCode (), но что, если roleName не является необходимым полем? Я почти уверен, что это не так сложно составить более сложный пример, который не может быть решен путем возврата hashCode одного из его полей.

Поэтому я хотел бы увидеть некоторые ссылки на связанные обсуждения или услышать ваш опыт решения этой проблемы. Спасибо!

Ответ 1

Bauer и King Book Java Persistence with Hibernate советует не использовать ключевое поле для equals и hashCode. Они советуют вам выбрать, какие будут поля бизнес-ключа объекта (если не было искусственного ключа) и использовать их для проверки равенства. Поэтому в этом случае, если имя роли не было необходимым полем, вы могли бы найти нужные поля и использовать их в комбинации. В случае кода, который вы публикуете, где указано имя роли, которое у вас есть помимо идентификатора, имя роли было бы тем, с чем я бы пошел.

Здесь цитата со страницы 398:

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

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

Простым способом, который я использую для создания метода equals и hashcode, является создание метода toString, который возвращает значения полей бизнес-ключа, а затем использовать их в методах equals() и hashCode(). CLARIFICATION: Это ленивый подход, когда меня не интересует производительность (например, внутренние веб-приложения rinky-dink), если ожидается, что производительность будет проблемой, тогда напишите сами методы или используйте средства генерации кода IDE.

Ответ 2

Прошу прощения, что в последнее время я кричу, но никто не упомянул об этом, и здесь есть серьезный недостаток.. Возможно, на самом деле два.

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

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

Это смертельно ошибочная практика: вы можете легко иметь два объекта с одинаковым значением roleName, но разные значения id.

Практика состоит в том, чтобы использовать те же самые поля для генерации значения hashcode, которые используются методом equals(), и в том же порядке. Ниже приведена моя замена для вашего метода hashcode:


public int hashCode () {
    return ((roleName==null) ? 0 : roleName.hashcode()); 
}

Примечание. Я не знаю, что вы использовали, используя поле id как hashcode, или то, что вы хотели сделать с полем id. Я вижу из аннотации, что он сгенерирован, но сгенерирован извне, поэтому класс, написанный, не выполняет контракт.

Если по какой-то причине вы оказываетесь в ситуации, когда этот класс исключительно управляется другим, который верно генерирует значения id для имен роликов, которые выполняют контракт, у вас не было бы функциональности проблема, но это все равно будет плохой практикой или, по крайней мере, иметь то, что люди называют "запахом кода". Помимо того факта, что в определении класса нет ничего, чтобы гарантировать, что класс используется только таким образом, hashcodes не являются идентификаторами, поэтому идентификаторы не являются хэш-кодами.

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

И как хорошее общее правило, если вам нужно это сделать, вы, вероятно, сделали ошибку дизайна. Не всегда, но, наверное. Одна из причин этого? Люди не всегда читают комментарии, поэтому даже если вы создадите совершенно функционирующую систему, со временем кто-то "неправильно использует" ваш класс и вызовет проблемы.

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

Ответ 3

Бизнес-ключ объекта может требовать его родительское (или другое отношение "один к одному" или "много-к-одному" ). В этих случаях вызов equals() или hashcode() может привести к удару базы данных. Помимо производительности, если сеанс закрыт, что приведет к ошибке. Я в основном отказался от попыток использовать бизнес-ключи; Я использую первичный идентификатор и избегаю использования несанкционированных объектов в картах и ​​наборах. Хорошо работает до сих пор, но, вероятно, это зависит от приложения (следите за тем, чтобы сохранить несколько дочерних элементов через родительский каскад). Иногда я использую отдельное бессмысленное ключевое поле, которое автоматически генерируется uuid в конструкторе или создателем объекта.

Ответ 5

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

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

Ответ 6

Ниже приведены простые инструкции по созданию методов hashCode, equals и toString с использованием разработчиков сообщества Apache.

хэш-код

  • Если два объекта равны в соответствии с методом equals(), они должны иметь одинаковое значение hashCode()
  • Возможно, что два разных объекта могут иметь один и тот же hashCode().
  • Пожалуйста, используйте уникальный бизнес-идентификатор для создания hashCode (это означает, что вы должны использовать какое-то уникальное свойство, представляющее бизнес-объект, например имя)
  • Hibernate Entity: не используйте Hibernate id для создания hashCode.
  • Вы можете вызвать .appendSuper(super.hashCode()), если ваш класс является подклассом

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
                .append(getName())
                .toHashCode();
    }
    

равно

  • Пожалуйста, сравните идентификатор бизнеса (это означает, что вы должны использовать какое-то уникальное свойство, представляющее бизнес-объект, например имя)
  • Hibernate Entity: не сравнивайте идентификатор Hibernate
  • Hibernate Entity: используйте getters при доступе к другому полю объекта, чтобы позволить Hibernate загрузить свойство
  • Вы можете вызвать .appendSuper(super.equals(other)), если ваш класс является подклассом

    @Override
    public boolean equals(final Object other) {
        if (this == other)
            return true;
        if (!(other instanceof TreeNode))
            return false;
        TreeNode castOther = (TreeNode) other;
        return new EqualsBuilder()
                .append(getName(), castOther.getName())
                .isEquals();
    }
    

ToString

  • Убедитесь, что toString не выбрасывает исключение NullPointerException.
  • Вы можете вызвать .appendSuper(super.toString()), если ваш класс является подклассом

    @Override
    public String toString() {
        return new ToStringBuilder(this)
                .append("Name", getName())
                .toString();
    }