回答
这是因为 equals()
和 hashCode()
之间存在两个契约关系:
- 如果两个对象相等(即
equals()
返回true
),那么它们的hashCode()
必须返回相同的整数值。 - 如果两个对象的
hashCode()
返回相同的值,并不要求这两个对象一定相等(即equals()
返回true
),但如果两个对象的hashCode()
返回不相同值,那要求这两个对象不相等(equals()
返回false
)。
基于这种契约关系,如果我们通过重写 equals()
来改变对象的相等逻辑,但是却没有重写 hashCode()
,那么这两个方法的契约关系就被破坏了。破坏了这种契约关系会导致我们无法正常使用基于散列的集合类(如 HashSet
, HashMap
, HashTable
):
- 在
HashSet
中,我们知道它是不能有重复数据的。如果equals()
被重写以改变相等性判断,而hashCode()
没有相应地被重写,那么即使两个对象根据equals()
方法是相等的,它们也可能同时存在于集合中,因为它们的哈希码不同,从而违反了集合不包含重复元素的性质。 - 在
HashMap
中,元素的存储位置(桶的位置)是根据其哈希码计算得出的。如果两个对象根据equals()
方法是相等的,但它们的哈希码不同,那么它们会被错误地存储在不同的桶中。
详解
equals()
和 hashCode()
是 Java 中顶级类 Object 的两个方法,我们先看 equals()
,定义如下:
public boolean equals(Object obj) {
return (this == obj);
}
该方法直接是对比两个对象的地址是否一样,这里我们关注注释:
红色标注地方的意思是:每当我们重写 equals()
时,就需要重写 hashCode()
,这样才能不违背 hashCode()
的约定:相等的对象必须具有相等的哈希值。
我们再看 hashCode()
:
hashCode()
是个 native 方法,它返回一个对象的哈希值,在 hashCode()
中有三条注释(红色不分),大致意思如下:
- 同一个对象多次调用
hashCode()
,只要该对象的信息没有被修改,该方法都必须返回相同的整数值。 - 两个对象如果通过
equals()
判定是相等的,那么这两个对象的hashCode()
返回的值必须相等。 - 两个对象如果通过
equals()
判定是不相等的,那么不要求hashCode()
返回不相等的整数值,但是要求拥有两个不同的哈希值的对象必须是不同的对象。
根据这三条规则,我们可以得到下面这张图:
这里有两个必须成立的条件(虚线部分):
- 相同的对象必须有相同的哈希值。
- 不同的哈希值必须是由不同的对象导致。
所以,到这里我们就应该明白: equals()
和 hashCode()
这两个方法根本就是配套使用的。对于任何一个 Java 对象,不论你是直接使用 Object 的 equals()
还是重写 equals()
,hashCode()
的本质都是为该 equals()
认定为相同的对象返回相同的哈希值。
下面我们以 HashSet 为例如果只重写 equals()
不重写 hashCode()
所带来的问题。
- 定义一个 User 类,重写它的
equals()
如下:
@Data
@ToString
@AllArgsConstructor
public class User {
private String name;
private Integer age;
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
User user = (User) o;
return user.getName().equals(this.getName())
&& user.getAge().equals(this.getAge());
}
}
- 简单测试下:
public class EqualsTest {
public static void main(String[] args) {
User user1 = new User("张三",15);
User user2 = new User("张三",15);
System.out.println("user1 == user2:" + (user1 == user2));
System.out.println("user1.equals(user2):" + (user1.equals(user2)));
System.out.println("user1.hashCode():" + user1.hashCode());
System.out.println("user2.hashCode():" + user2.hashCode());
}
}
结果:
user1 == user2:false
user1.equals(user2):true
user1.hashCode():1325547227
user2.hashCode():980546781
这里 user1.equals(user2)
为 true,我们就可以确认两个对象是相等的,我们将其添加到 HashSet 里面去:
public class EqualsTest {
public static void main(String[] args) {
User user1 = new User("张三",15);
User user2 = new User("张三",15);
HashSet<User> hashSet = new HashSet<>();
hashSet.add(user1);
hashSet.add(user2);
hashSet.forEach(System.out::println);
}
}
结果:
两个相等的对象添加到 HashSet 中为什么还是两个?他们应该去重的。我们根据源码去看:
HashSet 的底层实现是 HashMap,所以我们直接看 HashMap 的 put()
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
hash(key)
是计算 key 的哈希值,我们这里的 key 是 User 对象:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
其实就是调用 key 的 hashCode()
。如果我们不在 User 对象里面重写 hashCode()
,这里就调用了 Object 的 hashCode()
,返回的哈希值是对象的引用值的哈希码。
pubVal()
方法我们看下面一个地方就行:
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
这段代码是证明两个对象是否相等,它要求两者哈希值相当且二者地址值相等或调用equals()
认定相等。
在上面示例中,我们只能确认 user1 和 user2 调用 equals()
认定相等,他们的哈希值是不等的,所以 HashSet 就认定他们两个是不同的对象,尽管我们从业务逻辑上来说他们是相同。
所以,这里我们也重写
@Data
@ToString
@AllArgsConstructor
public class User {
//省略重复代码
@Override
public int hashCode() {
return Objects.hash(this.name,this.age);
}
}
我们再执行那个测试类: