当前位置: 首页 > 科技观察

面试题:为什么要重写hashcode和equals方法?

时间:2023-03-20 13:46:10 科技观察

我在面试初级Java开发的时候,经常被问到:你有没有重写过hashcode方法?很多考生直接说没写。我想,也许真的不是我写的,于是又问了一个问题来确认一下:你在使用HashMap的时候,是不是漏掉了关键部分的自定义对象?这时候考生说算了,所以两个问题的答案是矛盾的。最近问了下,这个问题一般都不太好回答,所以这篇文章我就简单的从哈希表入手,描述HashMap的数据存储规则,让大家自然而然的知道上面这个问题的答案。#1.使用Hash算法了解HashMap对象的效率。先回顾一下数据结构中的一个知识点:在一个长度为n(假设为10000)的线性表(假设为ArrayList)中,存储的是无序数;如果我们要找一个指定的数,就得从头遍历到尾。平均搜索次数为n除以2(此处为5000)。我们再观察一下Hash表(这里的Hash表纯粹是一个数据结构的概念,与Java无关)。它的平均搜索次数接近1,而且成本非常小。关键是在Hash表中,其存储的数据及其存储位置是与Hash函数相关联的。我们假设一个哈希函数是x*x%5。当然,实际情况中不可能使用这么简单的Hash函数。我们这里纯粹是为了图解方便,Hash表是一个长度为11的线性表,如果我们要把6放进去,那么我们先用Hash函数计算6,结果是1,所以我们把6放到索引号为1的位置。同理,如果我们要放数字7,经过Hash函数计算后,7的结果是4,那么就把它放到索引号为1的位置。index为4,这个效果如下图所示。这样做的好处非常明显。比如我们要找元素6,可以先通过Hash函数计算出6的索引位置,然后直接从1号索引开始找。但是,我们会遇到“哈希值冲突”的问题。例如,经过Hash函数计算后,7和8的Hash值是一样的。为此,JavaHashMap对象使用了“链地址法”的解决方案。结果如下图。具体方法是为所有Hash值为i的对象建立一个同义词列表。假设我们在放入8的时候,发现4号位置已经被占用了,那么我们就新建一个链表节点,放入8中。同理,如果我们要找8,那么我们发现4号索引是不是8,那么我们就顺着链表依次查找。虽然我们还是不能完全避免Hash值冲突的问题,但是Hash函数的设计是合理的,还是可以保证同义词列表的长度控制在一个合理的范围内。这里所说的理论知识并不是漫无目的的。在后面的文字中你可以清楚的了解到重写hashCode方法的重要性。#2.为什么要重写equals和hashCode方法当我们使用HashMap存储自定义类时,如果不重写这个自定义类的equals和hashCode方法,结果会和我们预期的不一样。让我们看一下示例WithoutHashCode.java。在第2到18行,我们定义了一个Key类;第3行定义了一个唯一的属性id。目前我们先注释掉第9行的equals方法和第16行的hashCode方法,在main函数的22行和23行定义了两个Key对象,这两个对象的id都为1,就好像是两把相同的钥匙,可以打开同一扇门。在第24行,我们通过泛型创建了一个HashMap对象。它的key部分可以存放Key类型的对象,value部分可以存放String类型的对象。在第25行,我们通过put方法将k1和一串字符放入hm中;在第26行,我们要使用k2从HashMap中获取值;这就像我们想用钥匙k1锁门,用k2开门。这是符合逻辑的,但是从目前的结果来看,第26行返回的结果并不是我们想象中的字符串,而是null。有两个原因——没有重写。一是hashCode方法没有改写,二是equals方法没有改写。当我们将k1放入HashMap时,首先会调用Key类的hashCode方法计算出它的哈希值,然后将k1放入哈希值所指示的内存位置。关键是我们没有在Key中定义hashCode方法。这里仍然调用了Object类的hashCode方法(所有类都是Object的子类),Object类的hashCode方法返回的hash值其实就是k1对象的内存地址(假设为1000)。如果再调用hm.get(k1),那么我们会再次调用hashCode方法(仍然返回k1的地址1000),然后我们就可以根据得到的hash值快速找到k1。但是我们这里的代码是hm.get(k2)。当我们调用Object类的hashCode方法(因为在Key中没有定义)计算k2的hash值时,实际上得到的是k2的内存地址(假设为2000)。由于k1和k2是两个不同的对象,它们的内存地址一定不能相同,也就是说它们的hash值一定不同,这就是为什么我们不能用k2的hash值来得到k1的原因。当我们去掉第16行和第17行的hashCode方法的注释时,会发现它返回的是id属性的hashCode值,其中k1和k2的id都是1,所以它们的hash值是相等的。让我们纠正保存k1并再次取k2的动作。在存储k1的时候,是根据它的id的hash值,假设这个是100,把k1这个对象放到对应的位置。取k2时,先计算它的hash值(由于k2的id也是1,所以这个值也是100),然后到这个位置去找。但是结果会出乎我们的意料:第100位已经有了k1,但是第26行的输出结果还是null。原因是Key对象的equals方法没有被重写。HashMap使用链地址的方式来处理冲突,也就是说在100位置,可能有多个对象以链表的形式存储。他们通过hashCode方法返回的hash值都是100,当我们通过k2的hashCode查找到100的位置时,确实会得到k1。但是,k1可能只和k2有相同的hash值,但不一定等于k2(k1和k2这两把钥匙不一定能打开同一扇门)。这时候就需要调用Key对象的equals方法来判断两个key是否相等。由于我们没有在Key对象中定义equals方法,系统不得不调用Object类的equals方法。由于Object的固有方法是根据两个对象的内存地址来判断的,所以k1和k2肯定是不相等的,这也是为什么在第26行通过hm.get(k2)获取到的还是null。解决这个问题,我们需要取消注释第9到14行的equals方法。在这个方法中,只要两个对象都是Key类型并且它们的id相等,它们就相等。#3.面试题的解释由于HashMap在项目中经常用到,所以面试的时候肯定会问这个问题。您是否覆盖了hashCode方法?你在使用HashMap的时候有没有重写hashCode和equals方法?你怎么写的?可以更改对象的哈希码吗?根据出题结果,我发现初级程序员普遍对这个知识点把握不好。重申一下,如果你想在HashMap的“键”部分存储一个自定义对象,你必须在这个对象中使用自己的equals和hashCode方法覆盖Object中的同名方法。