Должен ли 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()
следует использовать те же поля, чтобы избежать неожиданностей (как утверждается здесь: /questions/7378116/razlichnyie-polya-dlya-ravnyih-i-hesh-koda/7378118#7378118).
Вопрос
Есть ли опасность использования только подмножества полей, которые используются в 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) вы должны:
- Сначала удалите этот объект из набора;
- Затем измените объект;
- И, наконец, добавить его обратно в набор.
Это совершенно верно для меня. Предположим, у вас есть 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.
Поэтому я настоятельно рекомендую еще раз подумать о концепции равенства и изменчивости вашего класса объектов.