0%

为什么重写equals()就必须重写hashCode()

equals()hashCode()方法是java.lang.Object类中的两个方法,所有Java类都会继承这两个方法。本文解释了equals()函数和hashCode()函数的作用,以及它们之间的关系,为什么重写equals()就必须重写hashCode()函数。

equals()

大部分情况,我们自己定义的类会重写equals()方法,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class People{
private String name;

People() {}

People(String name) {
this.name = name;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
} else {
People p = (People)o;
return p.name.equals(this.name);
}
}
}

这时,如果我们创建两个name一样的People对象调用equals()方法判断它们是否相等,就会返回true。

1
2
3
4
5
6
7
public class Main {
public static void main(String[] args) {
People p1 = new People("Bob");
People p2 = new People("Bob");
System.out.println(p1.equals(p2));
}
}
1
true

hashCode()

Object中的hashCode()方法是native方法,也就是用C或者C++语言实现的,该方法通常用来将对象的内存地址转换为整数之后返回,返回的整数称为哈希码

举个例子:

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
People p1 = new People("Bob");
People p2 = new People("John");
System.out.println(p1 + " -> " + p1.hashCode());
System.out.println(p2 + " -> " + p2.hashCode());
}
}
1
2
People@776ec8df -> 2003749087
People@16b98e56 -> 381259350

当然这里的People@776ec8df并不是真正的内存地址,只是个标识。

学过数据结构哈希表的同学都知道,大部分情况下,不同的输入哈希函数会计算出不同的值,少数情况下,不同的输入哈希函数也可能得到相同的值;另外,相同的输入哈希函数计算出的值一定相同。也就是说,不同的对象调用hashCode()少数情况下可能得到相同的哈希码

equals()与hashCode()的关系

我们平时会使用类似HashSetHashMap这样的数据结构来编写程序,那么在插入一个数据的时候它们是如何判断数据是否相等的?这里以HashSet为例。

插入一个对象时,HashSet会先调用对象的hashCode()函数计算哈希码,如果哈希码与所有已加入集合的对象都不同,那么新加入的对象一定与原有对象不同,直接加入集合中;如果哈希码与某个已加入集合的对象哈希码相同,这两个对象也可能并不相同,再调用equals()来判断它们是不是真的相同,不同再加入集合。

这种实现方式大大减少了equals()函数的调用次数,提高了执行速度。

为什么重写equals()就必须重写hashCode()

打开Java官方文档查看java.lang.Object.hashCode()方法的说明

在官方文档中硬性规定了:

  • If two objects are equal according to the equals(Object) method, then calling the hashCode method on each of the two objects must produce the same integer result.

即:如果两个对象调用equals()方法返回true,那么这两个对象的hashCode()方法必须返回值相同的整数值。

我们依然以上述提到的People类为例,我们已经实现了equals()方法,但是并没有实现hashCode()方法,就会出现这种情况:两个对象调用equals()函数相等,但是它们的hashCode()并不相等。

1
2
3
4
5
6
7
8
public class Main {
public static void main(String[] args) {
People p1 = new People("Bob");
People p2 = new People("Bob");
System.out.println(p1.equals(p2));
System.out.println(p1.hashCode() == p2.hashCode());
}
}
1
2
true
false

所以我们必须重写hashCode()函数使得它们也相等。这是笔者随意给出一种重写方式:

1
2
3
4
@Override
public int hashCode() {
return name.hashCode();
}

再次执行Main类中的main()方法,结果就满足了官方的规定:

1
2
true
true

那么问题又来了,为什么官方要这样规定?

本文前部分已经解释了hashCode()函数的作用,相同的输入哈希函数计算出的值一定相同。既然我们重写了equals()方法认为这两个地址不同的对象逻辑上相同,那么两个对象计算出的哈希码也应该是相同的。如果我们不重写hashCode(),那么这两个逻辑上相同的对象在插入类似HashSet这样的数据结构中时,因为它们的地址不同,HashSet会认为它们是不同的对象而重复插入,这不是我们想要的结果,因为我们认为它们是相同的。

例如,两个People对象,但它们其实是同一个人Bob,如果不重写hashCode()函数,HashSet就会重复插入造成无法预料的后果。

1
2
People p1 = new People("Bob");
People p2 = new People("Bob");

Reference

IT无知君的博客 - CSDN博客

JavaGuide - GitHub