equals与hashCode方法重写

equals与hashCode重写

在某些场景,需要重写某个对象的equals()方法来满足我们的需求,重写equals()方法时一定注意要重写hashCode()方法。为什么重写equals()方法就一定要重写hashCode()方法呢?

例如,现在统计不同种类的书的数量,书分类的规则是:书名作者一样这些书就是相同的。首先定义Book对象,添加相应字段以及set、get方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Book{
private String name;
private String author;
public Book(String name, String author){
this.name = name;
this.author = author;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
  • 根据业务需求重写equals()方法,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean equals(Object object){
if(object == null){
return false;
}
if(object == this){
return true;
}
if(object instanceof Book){
Book that = (Book)object;
if(that.getName().equals(this.name) && that.getAuthor().equals(this.author)) {
return true;
}
}
return false;
}

首先编写测试代码测试equals是否能满足我们的需求:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args){
Book mathOne = new Book("math", "Dave");
Book mathTwo = new Book("math", "Dave");
Book engOne = new Book("English", "Dave");
System.out.println(mathOne.equals(mathTwo));
System.out.println(mathOne.equals(engOne));
mathOne.setName("English");
System.out.println(mathOne.equals(mathTwo));
System.out.println(mathOne.equals(engOne));
}

输出结果为:

1
2
3
4
5
6
7
"D:\Program Files\Java\jdk1.8.0_91\bin\java" ...
true
false
false
true

Process finished with exit code 0

可以看到,重写equals已满足我们最开始的需求,当书名与作者相同时,认为这两本书相同。为什么还要重写hashCode()方法呢?其实很多地方建议甚至强制要求这样写,是因为equals()重写的对象存入依赖hashCode()方法的容器类时就会发生问题,例如HashMap、HashSet等容器,很多方法都依赖对象的hash值。

  • 接下来验证不重写hashCode()方法会产生什么后果,是否能正常使用。我们将Book对象的实例存入HashSet容器中,验证HashSet去重是否能够成功。
1
2
3
4
5
6
7
8
Book mathThree = new Book("math", "Dave");
Book mathFour = new Book("math", "Dave");
HashSet<Book> hashSet = new HashSet<>();
hashSet.add(mathThree);
hashSet.add(mathFour);
for (Book book : hashSet) {
System.out.println(book.name);
}

输出结果为:

1
2
3
4
5
"D:\Program Files\Java\jdk1.8.0_91\bin\java" ...
math
math

Process finished with exit code 0

由上可发现,两次add均成功,并没有将两个书名、作者相同的Book对象实例当做相等来处理。Set内部添加了两个Book对象。查看HashSet的源码,追查原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 空set直接插入
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 判断元素是否已存在
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 循环查到
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

通过查看16行与30行代码发现,判断两个元素相等首先判断他们的hash值是否相等,而我们并没有重写hash值,故HashSet认为mathThree、mathFour这两个实例对象是不相同的,故HashSet会将两个相等的元素存进去。查看HashMap的源码也是如此,在判断两个Key元素是否相等时,首先判断Key的hash值是否相同,若hash不同则认为Key元素不相等。

  • 接下来我们重写Book对象的hashCode()方法,再次验证
1
2
3
4
5
6
7
8
@Override
public int hashCode(){
final int prime = 31;
int hash = 1;
hash = prime*hash + (this.name == null ? 0 : this.name.hashCode());
hash = prime*hash + (this.author == null ? 0 : this.author.hashCode());
return hash;
}

输出结果为

1
2
3
4
"D:\Program Files\Java\jdk1.8.0_91\bin\java" ...
math

Process finished with exit code 0

可以发现,重写hashCode()之后HashSet添加成功。HashSet将mathThree、mathFour作为“相等”的两个实例对象,故只添加了一个。

同样的我们如果只重写hashCode()而不重写equals()方法,因为无法满足业务上的需求:书名与作者相同就是同一本书。

  • 总结:在重写equals()方法时,必须重写hashCode()方法,否则对象将无法配合HashSet、HashMap、LinkedHashMap、TreeMap等容器正常使用。故强烈建议两个方法同时重写。