彻底理解java中HashMap的“冲突”问题及hashCode和equals方法重写问题

您所在的位置:网站首页 发生冲突的成语是什么 彻底理解java中HashMap的“冲突”问题及hashCode和equals方法重写问题

彻底理解java中HashMap的“冲突”问题及hashCode和equals方法重写问题

2024-07-17 13:35| 来源: 网络整理| 查看: 265

众所周知,HashMap是Java知识点里的重中之重,也是面试、尤其是中高级程序员面试中的必考点。扎扎实实把hashMap的底层原理搞清楚是十分必要的。首先,关于HashMap的底层原理,数组+链表(java8之后改为数组+链表+红黑树)存储结构,初始大小,负载因子,rehash()等等这些概念,相信大家都知道的八九不离十了,网上也有海量相关文章,这里就不再说了。先附一张HashMap原理图(数组+链表+红黑树),方便下面论述。

问题是,最近在复习这些知识的时候,发现了一个以前没有注意到的细节——到底什么情况下会发生“冲突/碰撞”?

首先,重申一下什么是冲突?冲突就是多个对象的hashCode相同,因此会排在数组(桶)同一个位置,但它们之间equals结果为false,所以是不同的对象,此时会以链表形式追加在后面。冲突会大大降低HashMap的存取性能,是要避免的(也是jdk8引入红黑树的原因)。

那么,什么时候会产生冲突?以往的资料里,包括此前自己的理解里,对此都是一笔带过——“当不同对象的hashCode相同时”,就会发生冲突。但现在再追问一句:“不同对象的hashCode什么情况下会相同?”顿时,感觉自己支支吾吾说不清楚了。当时自己的理解是:

1)没重写hashCode方法时,不是用内存地址值来计算hashCode方法吗?既然不同对象的地址值不同,那hashCode肯定不同啊,哪来的冲突?

2)重写了hashCode方法,一般也要重写equals方法,此时若hashCode相同,equals也相同,就是相同的对象,不会有冲突问题!

所以,到底何时会冲突??难道,仅限于“重写了hashCode方法而未重写equals方法”这一种怪异而例外的情形?

问了身边一些人,也都含混不清,看似理解,实则说不清楚。网上搜到的信息更是复制粘贴滥竽充数,答非所问,毫无自己的分析。偏偏我就想搞清楚。于是,经过几天的仔细研究、询问、代码测试,现在终于彻底搞懂了。下面详说。

不同对象的hashCode何时会相等?

1)重写hashCode方法时

众所周知,一个类,如果你用某个属性去重写了hashCode方法,那么当不同对象的该属性相等时,它们的hashCode也一样。包括String类,也重写了hashCode(以及equals)方法,当字符串内容相同时,hashCode相同。我们可以看一下hashCode方法的源码:

public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; // 这里的value就表示字符串的内容 for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }

相信这段代码大家都能看懂,不再详述。重写hashCode方法的情形,也不是本文重点。

2)未重写hashCode方法时

这才是本文的重点——当没有重写hashCode方法时,不同对象的hashCode值到底可不可能一样?看似一个简单的问题,实际上却未必简单。至少,会引发剧烈的争议。首先,我们来看一下关于这个问题的一篇帖子,也是被网上疯转的一篇(都是互相复制粘贴的,也搞不清哪一篇是源头了):

https://blog.csdn.net/n13151125/article/details/83623296

看这里:

好一个“肯定”不同,当初我看了这个帖子也想当然地认为“肯定不同”。而且身边不少人也是这样认为的。但这就大错特错了!为什么?

首先,hashCode方法返回的并不是对象在jvm上的内存地址值本身,而是通过一定算法,转化而来的一个int值。(而因为hashCode方法是一个native方法,我们并不能直接看到源码!)

这里就存在一个问题,内存上可能的地址值有多少个?经询问高手,得到的答案是,以64位系统为例,就是2的64次方个,而int的最大值是2的32次方,因此,理论上,int值不够分配给所有地址值,这就决定了会出现不同对象hashCode相等的情况。当然,这是个理论推测,为了验证它,我写了个小测试:

public static void main(String[] args) { Set hashCodeSet = new HashSet(); for (int i = 0; i < Integer.MAX_VALUE; i++) { Integer hashCode = new Object().hashCode(); if (hashCodeSet.contains(hashCode)){ System.out.println("出现重复hashCode值:" + hashCode + ",此时size为:" + hashCodeSet.size()); break; } hashCodeSet.add(hashCode); } }

运行结果如下(而且不管运行多少次,结果都是相同的):

出现重复hashCode值:2134400190,此时size为:105821

果不其然,才创建了10万多个对象,还远没有到亿级别,就出现了hashCode重复的现象。如果把break注释掉,则可以看到后面大量出现hashCode重复现象。这就证明,虽然Object类的hashCode方法是用内存地址作为依据,但得到的int返回值并不能保证唯一性!所以结论就是:即便不重写hashCode方法,不同对象的hashCode也可能相同!!

延伸:关于hashCode方法与equals方法重写问题

上面反复提到了重写hashCode方法问题,而一般提到这个,也必然会提到equals方法。这是java里面老生常谈的话题:为什么要同时重写这两个方法?什么时候要重写?不重写会怎么样?......关于这些,网上帖子也是大把抓,本文不再详述。这里重申一下几个结论:

若两个对象hashCode相等,则equals()不一定为true;若两个对象hashCode不相等,则equals()一定为false;若两个对象equals()为true,则hashCode一定相等;若两个对象equals()为false,则hashCode不一定不相等;

可以把hashCode理解为房间号,而equals理解为身份证号,住在同一个房间,不一定是同一个人,不住在同一个房间,必然不是同一个人。

这里要补充的一个问题就是:若重写其中一个方法,而不重写另一个,会怎样?

1)只重写hashCode方法,不重写equals方法

设想一下,一个Student类,只重写了hashCode方法(用name属性),现在有两个名为“张三”的student想往HashMap里放,因为它们的hashCode相等,在数组中的位置一样,HashMap会继续去调用equals方法比较两个对象是否相等,显然,两个对象的内存地址不同,equals结果为false,结果两个对象都被put进了map,这是不合理的。

2)只重写equals方法,不重写hashCode方法

再设想一下,如果String类只重写了equals()方法,用字符串内容作为判断对象相等的依据,而未重写hashCode方法,此时,两个内容都为"abc"的String对象,往hashMap里put的时候,第一步判断hashCode不同(大概率),则根本不会调用equals方法判断,直接就放了进去,map里就会有两个"abc",这也是不合理的。

显然,为了满足上述四个约束,就一定要同时重写这两个方法。

总结:再理解HashMap的“冲突”问题:

搞清了上述几个知识点,再来理解HashMap的冲突问题,就顺畅多了。什么时候会发生冲突?根据上面的原理分析,结论如下:

1)未重写hashCode和equals方法

此时,不同对象(这里指的是map中的key对象)大概率情况下hashCode不相等,但会有一定概率hashCode相等,然而此时它们的equals始终返回false(因为equals方法返回的是严格的内存地址值),为不同对象,也即会发生冲突,这些对象在首个对象后面以链表形式追加。

2)重写了hashCode方法,未重写equals方法

上面说过,此时若有两个“张三",本应第二个put不进去,结果因为equals方法返回false(因为内存地址不同),则发生了不必要的冲突,两个对象都put进去了,产生了链表。

3)重写了equals方法,未重写hashCode方法

不同对象hashCode不同(大概率),所以得到桶中的不同位置,不会发生冲突,也不会产生链表。但此时会导致业务逻辑上的不合理现象,本来重复的两个对象(如上面说的两个"abc")被当成不同对象put进去了,显然也是不合理的。

4)重写了hashCode和equals方法

此时,两个“张三”会因为hashCode相等,得到相同的数组(桶)的位置,然后调用equals方法,发现也是同一个对象,因此不会put进map。在get的时候,也能通过hashCode和equals方法定位到这个唯一的key对象,从而得到其value。可见,此时既不会发生冲突,也保证了key的唯一性,是完美的结果。

通过上面的分析,彻底理解了HashMap的原理以及冲突现象,也理解了为什么,要使用HashMap(以及HashSet)来存储的对象类一定要同时重写hashCode和equals方法。

延伸思考

就算重写了hashCode方法,就一定能保证不同对象的hashCode不同吗?

举个例子:一个User类,有一个类型为Long的id属性,我用id属性重写hashCode方法,现在遍历long的所有值来创建对象(Long的最大值为 9223372036854775807)。再怎么个算法,hashCode返回值也是个int(21亿多),又怎么能覆盖所有这些对象呢?所以这种场景必然会出现hashCode相同的情况。

当然,这种场景太极端了,业务中不大可能出现,这么多的对象,内存也装不下,不具有现实可能性。仅作为一种理论上的思考。



【本文地址】


今日新闻


推荐新闻


    CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3