Должен ли hashCode() использовать только подмножество неизменных полей из тех, которые используются в equals()?

ситуация

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

set.contains(object)
=> false

в то время как

set.stream().findFirst().get().equals(object)
=> true

Теперь я понимаю, что это связано с изменениями, которые были внесены в object после того, как он был добавлен к set который снова изменил свой хэш-код. contains затем смотрит на неправильный ключ и не может найти object,

Мои требования для реализации являются

  • изменяемые поля необходимы для правильной реализации equals()
  • безопасно использовать эти объекты в хэш Collections или же Maps такой пепел HashSet даже если они подвержены изменениям.

который противоречит соглашению о том, что

Вопрос

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

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

Я думаю, что это должно быть хорошо, потому что

  • контракт выполнен: равные объекты будут создавать один и тот же hashCode, тогда как один и тот же hashCode не обязательно означает, что объекты совпадают.
  • HashCode объекта остается прежним, даже если объект подвергается изменениям и, следовательно, будет найден в HashSet до и после этих изменений.

Связанные посты, которые помогли мне понять мою проблему, но не узнать, как ее решить: какие проблемы следует учитывать при переопределении equals и hashCode в Java? и различные поля для равных и хэш-код

4 ответа

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

Кажется, ваша проблема вызвана изменением объекта, пока он находится внутри набора, таким образом, что это изменяет функционирование hashCode() и / или equals(), Всякий раз, когда вы добавляете объект в HashSet (или как ключ в HashMap), вы не должны впоследствии изменять какие-либо поля этого объекта, которые используются equals() и / или hashCode(), В идеале все поля используются equals() должно быть final, Если они не могут быть, вы должны обращаться с ними, как будто они являются окончательными, пока объект находится в наборе.

То же самое относится и к TreeSet/TreeMap, но относится к полям, используемым compareTo(),

Если вам действительно нужно изменить поля, которые используются equals() (или compareTo() в случае TreeSet/TreeMap) вы должны:

  1. Сначала удалите этот объект из набора;
  2. Затем измените объект;
  3. И, наконец, добавить его обратно в набор.

Это совершенно верно для меня. Предположим, у вас есть Person:

 final int name; // used in hashcode
 int income; // name + income used in equals

name решает, куда будет идти запись (подумайте HashMap) или какое ведро будет выбрано.

Вы положили Person как Key внутри HashMap: в соответствии с hashcode это идет к некоторому ведру, второму например. Вы обновляете income и искать это Person на карте. В соответствии с hashcode это должно быть во втором ведре, но согласно equals его там нет

 static class Person {
    private final String name;

    private int income;

    public Person(String name) {
        super();
        this.name = name;
    }

    public int getIncome() {
        return income;
    }

    public void setIncome(int income) {
        this.income = income;
    }

    public String getName() {
        return name;
    }

    @Override
    public int hashCode() {
        return name.hashCode();
    }

    @Override
    public boolean equals(Object other) {
        Person right = (Person) other;

        return getIncome() == right.getIncome() && getName().equals(right.getName());
    }

}

И тест:

    HashSet<Person> set = new HashSet<>();
    Person bob = new Person("bob");
    bob.setIncome(100);
    set.add(bob);

    Person sameBob = new Person("bob");
    sameBob.setIncome(200);

    System.out.println(set.contains(sameBob)); // false

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

Пример, который вы приводите, совершенно легален; но тот, который вы связали, наоборот - он использует больше полей в hashcode делая это таким образом неправильно.

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

Контракт действительно будет выполнен. Контракт предусматривает, что .equal() объекты ВСЕГДА одинаковы .hashCode(), Обратное не обязательно должно быть правдой, и я удивляюсь, что некоторые люди и IDE одержимы идеей применения именно этой практики. Если бы это было возможно для всех возможных комбинаций, то вы бы обнаружили идеальную хеш-функцию.

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

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

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

  • Включение изменяемых полей в метод compareTo() может изменить знак сравнения, так что объект должен переместиться в другую ветвь дерева.
  • Исключение изменяемых полей в методе CompareTo () ограничивает вас наличием не более одного элемента столкновения в TreeSet.

Поэтому я настоятельно рекомендую еще раз подумать о концепции равенства и изменчивости вашего класса объектов.

Другие вопросы по тегам