情况
我需要覆盖equals()
并且建议我还使用相同的字段覆盖了hashCode()
方法。然后,当我查看一个集合时,它只包含一个对象,我得到了令人沮丧的结果
set.contains(object)
=> false
而
set.stream().findFirst().get().equals(object)
=> true
我现在明白了,这是由于在将object
添加到set
后对它所做的更改再次更改了其哈希代码。 然后contains
看错了键,找不到object
。
我对实施的要求是
- 需要可变字段才能正确实现
equals()
- 在基于哈希的
Collections
中安全地使用这些对象,或者Maps
此类灰HashSet
,即使它们容易更改。
这与公约相冲突
equals()
和hashCode()
应该使用相同的字段以避免意外(如此处所述:https://stackoverflow.com/a/22827702)。
问题
仅使用equals()
中使用的字段子集来计算hashCode()
而不是全部使用是否存在危险?
更具体地说,这意味着:equals()
使用对象的多个字段,而hashCode()
仅使用equals()
中使用的不可变字段。
我认为这应该没问题,因为
- 合约已完成:相等的对象将产生相同的哈希代码,而相同的哈希代码并不一定意味着对象是相同的。
- 对象的哈希代码保持不变,即使对象暴露于更改,因此将在这些更改之前和之后的
HashSet
中找到。
帮助我理解我的问题但不知道如何解决它的相关文章:在 Java 中覆盖 equals 和 hashCode 时应考虑哪些问题?以及 等号和哈希码的不同字段
hashCode()
可以使用equals()
使用的字段子集,尽管这可能会给您带来轻微的性能下降。
您的问题似乎是由于修改对象引起的,而对象仍在集合中,以改变hashCode()
和/或equals()
的功能的方式。每当将对象添加到 HashSet (或作为 HashMap 中的键)时,您随后不得修改equals()
和/或hashCode()
使用该对象的任何字段。理想情况下,equals()
使用的所有字段都应final
。如果不能,则必须在对象在集合中时将它们视为最终的。
TreeSet/TreeMap也是如此,但适用于compareTo()
使用的字段。
如果确实需要修改equals()
(或在 TreeSet/TreeMap 的情况下由compareTo()
)使用的字段,则必须:
- 首先,从集合中删除该对象;
- 然后修改对象;
- 最后将其添加回集合中。
合同确实会履行。合同规定.equal()
对象始终具有相同的.hashCode()
。相反的情况不一定是真的,我想知道有些人和 IDE 对应用这种做法的痴迷。如果这对于所有可能的组合都是可能的,那么你就会发现完美的哈希函数。
顺便说一句,IntelliJ 在生成哈希代码时提供了一个很好的向导,并通过分别处理这两种方法并允许区分您的选择来平等。显然,相反,即在hashCode()
中提供更多字段,在equals()
中提供更少的字段将违反合同。
对于HashSet
和类似的集合/地图,让hashCode()
仅使用equals()
方法中的字段子集是一个有效的解决方案。当然,您必须考虑哈希代码对于减少地图中的冲突有多大用处。
但请注意,如果您想使用像TreeSet
这样的有序集合,问题又回来了。然后,您需要一个从不为"不同"对象提供碰撞(返回零)的比较器,这意味着该集合只能包含其中一个碰撞元素。您的equals()
描述意味着将存在多个仅在可变字段中不同的对象,然后您将丢失:
- 在 compareTo() 方法中包含可变字段可以更改比较符号,以便对象需要移动到树中的不同分支。
- 排除 compareTo() 方法中的可变字段会限制您在 TreeSet 中最多有一个碰撞元素。
所以我强烈建议你再次考虑你的对象类的平等和可变性概念。
这对我来说完全有效。假设您有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
找到所有这些(来自子集或存储桶)——您的示例将有意义。