JAVA中为什么要一起重写equals和hashCode方法?
在Effective Java
一书中,提到了一点:*在每个覆盖了equals方法的类中,都必须覆盖hashCode方法。*为什么要有这个要求呢?
这一要求与HashMap,HashTable等基于hash的集合的工作机制有关,如果不同时提供equals
和hashCode
方法,可能会导致这类集合无法正常工作。
HashMap的工作机制
HashMap
是我们常用的一个集合类,它可以映射键值对,非常方便。
在后台,它把我们的键值对打包成一个Entry<key,value>
,然后将这个Entry
存起来。当使用键查找时,HashMap
会在存放Entry
的地方查找符合要求的键,然后返回存储的值。
存放的地方是一个桶,以4个大小的桶为例,其存放方式如下所示(暂不考虑java8的红黑树方式):
当一个元素过来时,HashMap会先根据key的hashCode
计算出需要存放在哪个位置,比如键a
本来的hashCode
的值为47
,通过 mod 4
(取余数)计算得到可以存放在位置3
上,然后发现位置3
不为空,找到位置3
的链表,然后对里面的元素依次进行equals
比较,发现相同的则认为是更新,否则则认为是新增一个节点,会将新的节点放在这个链表的头部。
hashCode
不正确对HashMap
的影响
从上面的简单介绍可以发现,HashMap
会使用hashCode
作为其判断桶里面位置的依据,如果hashCode
不正确,可能会有以下问题:
- 只实现了
equals
没有定义hashCode
,可能会导致put
和get
不正常,不是以想要的方式运行。 - 实现的
hashCode
不均匀散列,导致数据都堆到一个位置,影响HashMap
的性能。
下面详细介绍这两种情况。
只实现了equals
没有定义hashCode
只实现了equals
,hashCode
就会是Object
默认的实现,一般是每个不同的对象的hashCode
都不一样。下面是一个简单的例子:
class Person{
private String name;
public Person(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person person)) return false;
return Objects.equals(name, person.name);
}
}
这个类只有一个字段,而且只实现了equals
,下面是把这个类当成key,来存放数据,我们测试一下:
public static void main(String[] args) {
Map<Person, String> testMap = new HashMap<>();
testMap.put(new Person("Zhang San"), "test val");
System.out.println(testMap.get(new Person("Zhang San")));
}
我们用new Person("Zhang San")
作为key,存了一个值test val
,然后尝试取出这个值,结果却大出所料:有时输出为null
,有时输出test val
,也就是说,会在某些情况下无法取出这个值。发生了什么呢?
还是用刚才的图来说明一下发生了什么:
我们存入时,假设键new Person("Zhang San")
的hashCode
为1
,存入位置1
,取数据时,new了一个新的对象,hashCode
为2
(默认每个对象的hashCode
都不一样),它落到了位置2
,这时发现位置2
没有数据,就返回null
了。当取数据new的对象的散列值为类似5
这类时,计算的存放位置为1
,发现1
里面有数据,把链表里面的数据取出来做比较,第一个equals
就相等了,就可以返回test val
。
所以,在实现equals
时,一定要实现hashCode
。
实现的hashCode
不均匀散列
先说正确的实现方式:可以直接使用Objects.hash
方法,实现的结果就比较好了。比如上面的例子可以实现为:return Objects.hash(name);
。
在IDEA中,还可以直接在类里面使用快捷键Alt+Insert
,在弹出窗口选择equals() 和 hashCode()
,直接一键生成。
继续介绍不均匀的散列,比如我们实现了一个散列函数: return 10
。还是上面的例子,我们会发现所有的数据都会映射到位置2,结果查找数据就成了链表查找,时间复杂度从O(1)降低到了O(n),效率会非常差。
总结
综上所述,实现equals
就必须要实现hashCode
方法,否则会导致基于hash
的一些集合无法正确工作。