目录
一、通俗解释
无论是对于初学者还是面试,这个问题都是一个经典。我们这里使用通俗易懂也就是俗称的“说人话”来解释为什么要重写hashCode()和equals()方法。
首先,以HashMap为例,我们简单了解一下其内部结构。HashMap底层采用了数组+链表的构造,其中jdk1.8开始还使用了红黑树。简图如下:
HashMap中每个单元位置都是一个Entry,又称为桶(bucket),其中jdk1.8是Node类型的数组。当算出的元素下标位置相同时,其实可能value不同的,那这时候就引入了链表,将位置相同但value不同的元素挂在链表上,且jdk1.8中还在链表基础上进行了红黑树的转化。关于HashMap日后有时间详谈,此处不赘述。读者只需要知道HashMap大致是这样一个结构。
实际上HashMap采用了hashCode()方法计算出哈希码,再通过哈希码来计算出存入元素的下标位置。而当数组下标位置相同,而value不同这种情况叫做“哈希冲突”。HashMap对于这种冲突的解决方案就是引入了链表,这种解决方案叫做“链地址法”,属于解决哈希冲突的一种。哈希冲突的解决还有其他方法,此处也不展开讲了。
然后,我们回到我们的主题:为什么要重写hashCode()和equals()方法?我们假设现在有一排抽屉,编号:0、1、2...,然后每个抽屉又分为若干小格子,每个格子可以存放一把钥匙。很显然,抽屉就相当于上述HashMap的数组,格子组成的列就相当于上述HashMap的链表。简图如下:
假如小明的老爹现在只存放了唯一一把可以存放开启宝藏之门的钥匙,放在抽屉1的格子c中。而小明不知道这些,他只从老爹那里得到一套算法,老爹说我给你一个hashCode()算法,你能据此算出这把钥匙在哪个抽屉里,然后你看所有抽屉、所有格子的钥匙吧,有很多还长得挺像,但是呢你要是随便拿出一把去开启宝藏之门还可能出问题。
我们知道如果不重写hashCode()和equals()方法,默认使用的是Object类的原生方法。关于这两个方法原生态长什么样,我们也简单看一下:
默认的equals()方法比较的是内存地址;而默认的hashCode()方法返回的是对象的内存地址转换成的一个整数,实际上指的的也是内存,两个方法可以理解为比较的都是内存地址。
也就是说,如果我们不重写,拿着我们理想中的钥匙形象然后通过hashCode()和equals()方法去计算,基本就拿不到抽屉1的格子c中的真正钥匙!这就是为什么要重写这两个方法了。重写是为了按照我们的要求去找到我们想要的结果。也就是我们希望按照我们已知的key去HashMap里面找到我们想要的value,不重写的话,它是找不到我们想要的结果的。
那我们只重写equals()方法而不重写hashCode()可行?那我们可以想一想,如果我只拿着equals()方法区找到与我们理想的钥匙长得一样的钥匙行不行?我们前面怎么说的呢?我们说不仅要求钥匙要跟我们需要的长得像,还需要这把钥匙必须是抽屉1的格子c中的,否则不一定能打开。你拿着抽屉5的格子a中的钥匙,你说长得一样,那不行。所以,这就很还懂了,为什么要求重写equals()方法的时候也最好重写hashCode()方法。
二、案例说明
假设现在我们真正的钥匙叫做myKey1,而我们通过目标构图设计出一个我们想要找的钥匙myKey2。然后,我们拿着这把钥匙去找,能找到开启宝藏之门的钥匙吗?转换到代码中就是以下myKey2能够得到我们通过myKey1这把钥匙塞进去的内容"This is real key"这句话吗?
public class Test {
public static void main(String[] args) {
MyKey myKey1 = new MyKey("001");//这是小明爸爸存放的钥匙
MyKey myKey2 = new MyKey("001");//这是我们的钥匙目标构图,我们像拿着该钥匙构图去找真正的钥匙
Map<MyKey, String> map = new HashMap<>();
map.put(myKey1,"This is real key");
System.out.println(map.get(myKey2));//拿着我们计算出的目标构图能否拿到我们想要的真正的钥匙呢?
}
}
class MyKey{
private String uniqueMark;//用于标记钥匙的唯一标识
public MyKey(String uniqueMark) {
this.uniqueMark = uniqueMark;
}
}
运行结果如下:
null
也就是在我们没有重写hashCode()和equals()的情况下,并不能得到我们想要的结果!那我们重写hashCode()和equals()吧!
此处注意,重写hashCode()和equals()方法是在哪里写?是谁作为HashMap的key,那我们就重写这个对象的hashCode()和equals()方法。因此,本例要到MyKey这个类中去重写。如下:
@Override
public int hashCode() {
return Objects.hash(uniqueMark);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyKey myKey = (MyKey) o;
return Objects.equals(uniqueMark, myKey.uniqueMark);
}
再次得到运行结果如下:
This is real key
很显然,重写了hashCode()和equals()方法之后就得到了我们想要的结果。
那么,如果只重写equals()方法而不重写hashCode()会出现什么结果呢?我们将上述hashCode()暂时去掉,重新得到运行结果如下:
null
可以看出,这样会引起一些列方法在HashMap中都得不到我们想要的结果,也就是上面我们说的,仅仅比较两把钥匙长得是否一样但不去保证它们所在的位置是不是一样是没有用的。
实际上,Java中有一条规定:相等的对象必须要具有相同的hashCode(哈希码)。也就是说,通过两个对象调用的equals()方法得到的结果相同也就是为true的话,则它们调用hashCode()的结果也必须相同。因此,重写equals()方法也应该要重写hashCode()方法。
三、总结
1.集合中涉及查找、比较等场景,需要同时重写hashCode()和equals()方法,且不能只重写后者而不重写前者。因为hashCode()方法决定了元素的位置,equals()方法决定了元素的内容。判断两个对象是否相等时,既要判断位置是否相同,也需要判断元素内容是否相同。
2.相等的对象必须要具有相同的hashCode,即通过两个对象调用的equals()方法得到的结果相同也就是为true的话,则它们调用hashCode()的结果也必须相同。因此,重写equals()方法也应该要重写hashCode()方法。
3.建议自动重写,不要手动重写,避免问题多多。