(h = key.hashCode()) ^ (h >>> 16)
hashCode就hashCode嘛,为啥还要>>>16,这个 ^ 又是啥,不着急一个一个来说
hashMap我们知道默认初始容量是16,也就是有16个桶,那hashmap是通过什么来计算出put对象的时候该放到哪个桶呢
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
上面是hashmap的getNode方法,对hashmap源码有兴趣的同学自行研究,我们今天主要看这一句:(n - 1) & hash
也就是说hashmap是通过数组长度-1&key的hash值来计算出数组下标的,这里的hash值就是上面(h = key.hashCode()) ^ (h >>> 16)计算出来的值
不要慌不要慌不要慌,看不懂没关系,我们现在总结下目前的疑问
为什么数组长度要 - 1,直接数组长度&key.hashCode不行吗
为什么要length-1 & key.hashCode计算下标,而不是用key.hashCode % length
为什么要^运算
为什么要>>>16
先说结论
数组长度-1、^运算、>>>16,这三个操作都是为了让key在hashmap的桶中尽可能分散
用&而不用%是为了提高计算性能
我们先看下如果数组长度不-1和不进行>>>16运算造成的结果,知道了结果我们后面才来说为什么,这样子更好理解
log.info(“数组长度不-1:{}”, 16 & “郭德纲”.hashCode());
log.info(“数组长度不-1:{}”, 16 & “彭于晏”.hashCode());
log.info(“数组长度不-1:{}”, 16 & “李小龙”.hashCode());
log.info(“数组长度不-1:{}”, 16 & “蔡徐鸡”.hashCode());
log.info(“数组长度不-1:{}”, 16 & “唱跳rap篮球鸡叫”.hashCode());
log.info(“数组长度-1但是不进行异或和>>>16运算:{}”, 15 & “郭德纲”.hashCode());
log.info(“数组长度-1但是不进行异或和>>>16运算:{}”, 15 & “彭于晏”.hashCode());
log.info(“数组长度-1但是不进行异或和>>>16运算:{}”, 15 & “李小龙”.hashCode());
log.info(“数组长度-1但是不进行异或和>>>16运算:{}”, 15 & “蔡徐鸡”.hashCode());
log.info(“数组长度-1但是不进行异或和>>>16运算:{}”, 15 & “唱跳rap篮球鸡叫”.hashCode());
log.info(“数组长度-1并且进行异或和>>>16运算:{}”, 15 & (“郭德纲”.hashCode()^(“郭德纲”.hashCode()>>>16)));
log.info(“数组长度-1并且进行异或和>>>16运算:{}”, 15 & (“彭于晏”.hashCode()^(“彭于晏”.hashCode()>>>16)));
log.info(“数组长度-1并且进行异或和>>>16运算:{}”, 15 & (“李小龙”.hashCode()^(“李小龙”.hashCode()>>>16)));
log.info(“数组长度-1并且进行异或和>>>16运算:{}”, 15 & (“蔡徐鸡”.hashCode()^(“蔡徐鸡”.hashCode()>>>16)));
log.info(“数组长度-1并且进行异或和>>>16运算:{}”, 15 & (“唱跳rap篮球鸡叫”.hashCode()^(“唱跳rap篮球鸡叫”.hashCode()>>>16)));
数组长度不-1:0
数组长度不-1:0
数组长度不-1:16
数组长度不-1:16
数组长度不-1:16
数组长度-1但是不进行异或和>>>16运算:8
数组长度-1但是不进行异或和>>>16运算:14
数组长度-1但是不进行异或和>>>16运算:8
数组长度-1但是不进行异或和>>>16运算:2
数组长度-1但是不进行异或和>>>16运算:14
数组长度-1并且进行异或和>>>16运算:4
数组长度-1并且进行异或和>>>16运算:14
数组长度-1并且进行异或和>>>16运算:7
数组长度-1并且进行异或和>>>16运算:13
数组长度-1并且进行异或和>>>16运算:2
一下就看出区别了哇,第一组返回的下标就只有0和16,第二组也只有2、8、14,第三组的下标就很分散,这才是我们想要的
这结合hashMap来看,前两组造成的影响就是key几乎全部怼到同一个桶里,及其不分散,用行话讲就是有太多的hash冲突,这对hashMap的性能有很大影响,hash冲突造成的链表红黑树转换那些具体的原因这里就不展开说了
而且!!
而且!!
而且!!
如果数组长度不 - 1,刚上面也看到了,会返回16这个下标,数组总共长度才16,下标最大才15,16越界了呀
知道了结果,现在说说其中的玄学
1、为什么数组长度要 - 1,直接数组长度&key.hashCode不行吗?
我们先不考虑数组下标越界的问题,hashMap默认长度是16,看看16的二进制码是多少
log.info(“16的二进制码:{}”,Integer.toBinaryString(16));
//16的二进制码:10000,
再看看key.hashCode()的二进制码是多少,以郭德纲为例
log.info(“key的二进制码:{}”,Integer.toBinaryString(“郭德纲”.hashCode()));
//key的二进制码:10001011000001111110001000
length & key.hashCode() => 10000 & 10001011000001111110001000
位数不够,高位补0,即
0000 0000 0000 0000 0000 0001 0000
&
0010 0010 1100 0001 1111 1000 1000
&运算规则是第一个操作数的的第n位于第二个操作数的第n位都为1才为1,否则为0
所以结果为0000 0000 0000 0000 0000 0000 0000,即 0
冷静分析,问题就出在16的二进制码上,它码是10000,只有遇到hash值二进制码倒数第五位为1的key他们&运算的结果才不等于0,这句话好好理解下,看不懂就别强制看,去摸会儿鱼再回来看
再来看16-1的二进制码,它码是1111,同样用郭德纲这个key来举例
(length-1) & key.hashCode() => 1111 & 10001011000001111110001000
位数不够,高位补0,即
0000 0000 0000 0000 0000 0000 1111
&
0010 0010 1100 0001 1111 1000 1000
&运算规则是第一个操作数的的第n位于第二个操作数的第n位都为1才为1,否则为0
所以结果为0000 0000 0000 0000 0000 0000 1000,即 8
如果还看不出这其中的玄机,你就多搞几个key来试试,总之记住,限制它们&运算的结果就会有很多种可能性了,不再受到hash值二进制码倒数第五位为1才能为1的限制
2、为什么要length-1&key.hashCode计算下标,而不是用key.hashCode%length?
这个其实衍生出三个知识点
1、其实(length-1)&key.hashCode计算出来的值和key.hashCode%length是一样的
log.info(“(length-1)&key.hashCode:{}”,15&“郭德纲”.hashCode());
log.info(“key.hashCode%length:{}”,“郭德纲”.hashCode()%16);
// (length-1)&key.hashCode:8
// key.hashCode%length:8
那你可能更蒙逼了,都一样的为啥不用%,这就要说到第二个知识点
2、只有当length为2的n次方时,(length-1)&key.hashCode才等于key.hashCode%length,比如当length为15时
log.info(“(length-1)&key的hash值:{}”,14&“郭德纲”.hashCode());
log.info(“key的hash值%length:{}”,“郭德纲”.hashCode()%15);
// (length-1)&key.hashCode:8
// key.hashCode%length:3
可能又有小朋友会思考,我不管那我就想用%运算,要用魔法打败魔法,请看第三点
3、用&而不用%是为了提高计算性能,对于处理器来讲,&运算的效率是高于%运算的,就这么简单,除此之外,除法的效率也没&高
3、为什么要进行^运算,|运算、&运算不行吗?
这是异或运算符,第一个操作数的的第n位于第二个操作数的第n位相反才为1,否则为0
我们多算几个key的值出来对比
//不进行异或运算返回的数组下标
log.info(“郭德纲:{}”, Integer.toBinaryString(“郭德纲”.hashCode()));
log.info(“彭于晏:{}”, Integer.toBinaryString(“彭于晏”.hashCode()));
log.info(“李小龙:{}”, Integer.toBinaryString(“李小龙”.hashCode()));
log.info(“蔡徐鸡:{}”, Integer.toBinaryString(“蔡徐鸡”.hashCode()));
log.info(“唱跳rap篮球鸡叫:{}”, Integer.toBinaryString(“唱跳rap篮球鸡叫”.hashCode()));
00001000101100000111111000 1000
00000101110000001000011010 1110
00000110001111100100010011 1000
00000111111111111100010111 0010
10111010111100100011001111 1110
进行&运算,看下它们返回的数组下标,length为16的话,只看后四位即可
8
14
8
2
14
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
分享
首先分享一份学习大纲,内容较多,涵盖了互联网行业所有的流行以及核心技术,以截图形式分享:
(亿级流量性能调优实战+一线大厂分布式实战+架构师筑基必备技能+设计思想开源框架解读+性能直线提升架构技术+高效存储让项目性能起飞+分布式扩展到微服务架构…实在是太多了)
其次分享一些技术知识,以截图形式分享一部分:
Tomcat架构解析:
算法训练+高分宝典:
Spring Cloud+Docker微服务实战:
最后分享一波面试资料:
切莫死记硬背,小心面试官直接让你出门右拐
1000道互联网Java面试题:
Java高级架构面试知识整理:
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
M7-1712750070643)]
算法训练+高分宝典:
[外链图片转存中…(img-nwNaxPjY-1712750070643)]
Spring Cloud+Docker微服务实战:
[外链图片转存中…(img-IUCp7gEz-1712750070644)]
最后分享一波面试资料:
切莫死记硬背,小心面试官直接让你出门右拐
1000道互联网Java面试题:
[外链图片转存中…(img-zJyTzHq2-1712750070644)]
Java高级架构面试知识整理:
[外链图片转存中…(img-aIjx1CvB-1712750070644)]
一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-Aa2O5OAR-1712750070645)]