快来我悄悄的给你说几个HashCode的破事哈希游戏。
哈希游戏作为一种新兴的区块链应用,它巧妙地结合了加密技术与娱乐,为玩家提供了全新的体验。万达哈希平台凭借其独特的彩票玩法和创新的哈希算法,公平公正-方便快捷!万达哈希,哈希游戏平台,哈希娱乐,哈希游戏
对咯,我这里就是以 HashMap 为切入点,给大家讲一下 Hash 冲突。
假设现在我们有个值为 [why技术] 的 key,经过 Hash 算法后,计算出值为 1,那么含义就是这个值应该放到数组下标为 1 的地方。
但是如图所示,下标为 1 的地方已经挂了一个 eat 的值了。这个坑位已经被人占着了。
其实 hash 冲突也就是这么一回事:不同的对象经过同一个 Hash 算法后得到了一样的 HashCode。
因为我画图的时候犹豫了大概 0.3 秒,往链表上挂的时候,我到底是使用头插法还是尾插法呢?
前面我们知道了,Hash 冲突的根本原因是不同的对象经过同一个 Hash 算法后得到了一样的 HashCode。
那么我问你:怎么才能快速弄两个 HashCode 一样的 String 呢?
我先问你:长度为 1 的两个不一样的 String,比如下面这样的代码,会不会有一样的 HashCode?
我们接着往下梳理,看看长度为 2 的 String 会不会出现一样的 HashCode?
要回答这个问题,我们要先看看 String 的 hashCode 计算方法,我这里以 JDK 8 为例:
类似于小学课本中一元二次方程中的未知数 x 和 y,我们需要带入到上面的 hashCode 方法中去计算。
因为,由上可得:对于任意两个字符串 xy 和 ab,如果它们满足 x-a=1,即第一个字符的 ASCII 码值相差为 1,同时满足 b-y=31,即第二个字符的 ASCII 码值相差为 -31。那么这两个字符的 hashCode 一定相等。
都已经说的这么清楚了,这样的组合对照着 ASCII 码表来找,不是一抓一大把吗?
我们在稍微加深一点点难度。假设我要构建 2 个以上 HashCode 一样的字符串该怎么办?
Aa 和 BB 的 HashCode 是一样的。我们把它两一排列组合,那不还是一样的吗?
再比如我之前《震惊!ConcurrentHashMap里面也有死循环?》这篇文章中出现过的例子,AaAa,BBBB:
4*2=8 种组合方式,我们又能得到 8 个 hashCode 一样的字符串了。
如果我们以 Aa,BB 为种子数据,经过多次排列组合,可以得到任意个数的 hashCode 一样的字符串。字符串的长度随着个数增加而增加。
通过上面的代码,我们就可以生成任意多个 hashCode 一样的字符串了。
有了这些 hashCode 一样的字符串,我们把这些字符串都放到HashMap 中,代码如下:
最后这个 HashMap 的长度会经过两次扩容。扩容之后数组长度为 64:
看到了吧,刺不刺激,长度为 64 的数组,存 14 个数据,只占用了 3 个位置。
所以,这样就算是 hack 了 HashMap。恭喜你,掌握了一项黑客攻击技术:hash 冲突 Dos 。
如果你想了解的更多。可以看看石头哥的这篇文章:《没想到 Hash 冲突还能这么玩,你的服务中招了吗?》。
如果没有,那么我再给你提示一下:数组下标为 32 的位置下,挂了一个长度为 8 的链表。
所以,在当前的案例中,数组下标为 32 的位置下挂的不应该是一个链表,而是一颗红黑树。
这是不对的。链表转红黑树的阈值是节点大于 8 个,而不是等于 8 的时候。
也就是说需要再来一个经过 hash 计算后,下标为 32 的、且 value 和之前的 value 都不一样的 key 的时候,才会触发树化操作。
没有骗你吧?从上面的图片可以清楚的看到,第 8 个节点还是一个普通的 node。
不信,我们再多搞一个 hash 冲突进来,带你亲眼看一下,代码是不会骗人的。
当第 9 个元素 BBBBAa 落进来的时候,会走到 treeifyBin 方法中去,但是不会触发树化操作,只会进行扩容操作。
所以,从下面的截图,我们可以看到,标号为 ① 的地方,数组长度变成了 32,链表长度变成了 9 ,但是节点还是普通 node:
但是偶尔会有那么几次,可能会把实体类当做 key 放到 HashMap 中去。
初始状态下,HashMap 中已经有一个名叫 why 的 7 岁小朋友了,他家住大南街,家里的交通工具是自行车。
然后,有一天他告诉老师,他搬家了,搬到了滨江路去,而且家里的自行车换成了摩托车。
更新完了之后,他们班上出现了两个叫 why 的 7 岁小朋友了,一个住在大南街,一个住在滨江路。
因此,标号为 ③ 的地方,经过 hash 计算后得出的 tab 数组下标,对应的位置为 null。不会进入 if 判断,这里返回为 null。
所以,如果我们 hashCode 和 equals 方法都没有重写,那么就会出现下面示意图的情况:
如果,我们重写了 hashCode,没有重写 equals 方法,那么就会出现下面示意图的情况:
总之一句话:在 HashMap 中,如果用对象做 key,那么一定要重写对象的 hashCode 方法和 equals 方法。否则,不仅不能达到预期的效果,而且有可能导致内存溢出。
比如上面的示例,我们放到循环中去,启动参数我们加上 -Xmx10m,运行结果如下:
因为每一次都是 new 出来的 student 对象,hashCode 都不尽相同,所以会不停的触发扩容的操作,最终在 resize 的方法抛出了 OOM 异常。
根据民间的说法,如果土拨鼠在 2 月 2 号出洞时见到自己的影子,然后这个小东西就会回到洞里继续冬眠,表示春天还要六个星期才会到来。如果见不到影子,它就会出来觅食或者求偶,表示寒冬即将结束。
这就呼应上了,通过判断土拨鼠出洞的时候是否能看到影子,从而判断冬天是否结束。
关于 HashCode 方法,《Java编程思想(第4版)》里面是这样写的:
作者说这个公式是从《Effective Java(第1版)》的书里面拿过来的。
《Effective Java(第1版)》太久远了,我这里只有第 2 版和第 3 版的实体书。
我想去下载最早的 JDK 版本去验证一下的,但是网上翻了个底朝天,没有找到合适的。
用方框框起来的部分想要表达的东西是一模一样的,只是对象从 37 变成了 31 。
而为什么从 37 变成 31 ,作者在第二版里面解释了,也就是我用下划线 有个很好的特许,即用位移和减法来代替乘法,可以得到更好的性能:
31*i==(i5)-1。现代的虚拟机可以自动完成这种优化。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,可以在留言区提出来,我对其加以修改。