作者 | Java3y
来源 | Java3y
一、HashMap剖析
首先看看HashMap的顶部注释说了些什么:
![3062a9c5be9faa15855a5a687c08cc86.png](https://i-blog.csdnimg.cn/blog_migrate/da42c58d98f1fbf00da60f36d401c755.jpeg)
再来看看HashMap的类继承图:
![b1267bb6c7139184edbc909a554c746c.png](https://i-blog.csdnimg.cn/blog_migrate/b051684b26e15b27a94764637ade6e7c.png)
下面我们来看一下HashMap的属性:
![acb0507abee67a5bc5351e4a30e0b411.png](https://i-blog.csdnimg.cn/blog_migrate/54bd7aa8b7323d9fc50c99b2d55b8cf2.jpeg)
成员属性有这么几个:
![6c39804216d39ce3448eebf990962fd1.png](https://i-blog.csdnimg.cn/blog_migrate/5ab0e1d2e5e60313d39ca94cf05daeaa.png)
再来看一下hashMap的一个内部类Node:
![a17355f818d51a3926b1800a553bf25f.png](https://i-blog.csdnimg.cn/blog_migrate/81692cd470756a85fb8cef0b2a2bf70a.jpeg)
我们知道Hash的底层是散列表,而在Java中散列表的实现是通过数组+链表的~
再来简单看看put方法就可以印证我们的说法了:数组+链表-->散列表
![4e0d18fafb51984077531d51b504e94d.png](https://i-blog.csdnimg.cn/blog_migrate/bf154afb99dbdd2d9c81f76ed1f55fa6.png)
我们可以简单总结出HashMap:
无序,允许为null,非同步
底层由散列表(哈希表)实现
初始容量和装载因子对HashMap影响挺大的,设置小了不好,设置大了也不好
1.1HashMap构造方法
HashMap的构造方法有4个:
![ec75cd05df9a1ed3ac96b38e1bd19715.png](https://i-blog.csdnimg.cn/blog_migrate/f377f2df40fce88cfd62525f6eb385e6.png)
![f4ead84925cedd8a653660ff43f8390a.png](https://i-blog.csdnimg.cn/blog_migrate/d71ed3f9822e8fb0cc7b202483d333ba.jpeg)
在上面的构造方法最后一行,我们会发现调用了tableSizeFor()
,我们进去看看:
![2633df8f1709fccddd41b8376c660bdd.png](https://i-blog.csdnimg.cn/blog_migrate/8e26fcb4a4ca29cb8869e8060ef55af8.jpeg)
这是位运算算法,具体流程可参考:
https://www.cnblogs.com/loading4/p/6239441.html
https://blog.csdn.net/fan2012huan/article/details/51097331
看完上面可能会感到奇怪的是:为啥是将2的整数幂的数赋给threshold?
threshold这个成员变量是阈值,决定了是否要将散列表再散列。它的值应该是:
capacity * load factor
才对的。
其实这里仅仅是一个初始化,当创建哈希表的时候,它会重新赋值的:
![e34d7724e7599044962eb73f9e3555d2.png](https://i-blog.csdnimg.cn/blog_migrate/8f39ca502fca89d93aaecc9845dddc22.jpeg)
至于别的构造方法都差不多,这里我就不细讲了:
![99214ea0c738e0ab21f06c12e0694a75.png](https://i-blog.csdnimg.cn/blog_migrate/36ae28cc4145b864cec38edeb5a93cbb.jpeg)
1.2put方法
put方法可以说是HashMap的核心,我们来看看:
![464f28b21ee5a55a1d28bd1ec2f65f23.png](https://i-blog.csdnimg.cn/blog_migrate/d319b959da84302a3644b966a39ac1fa.jpeg)
我们来看看它是怎么计算哈希值的:
![0ef8caaa18f7969f8fb984356905c1e1.png](https://i-blog.csdnimg.cn/blog_migrate/d30d1ef79dadc7457d935bb5fae4ea84.jpeg)
为什么要这样干呢??我们一般来说直接将key作为哈希值不就好了吗,做异或运算是干嘛用的??
我们看下来:
![1a7e4353f30d62c6ae10e48e2d3e706f.png](https://i-blog.csdnimg.cn/blog_migrate/f779b72999e0f9ddbf6612fce3e31c36.jpeg)
我们是根据key的哈希值来保存在散列表中的,我们表默认的初始容量是16,要放到散列表中,就是0-15的位置上。也就是tab[i = (n - 1) & hash]
。可以发现的是:在做&
运算的时候,仅仅是后4位有效~那如果我们key的哈希值高位变化很大,低位变化很小。直接拿过去做&
运算,这就会导致计算出来的Hash值相同的很多。
而设计者将key的哈希值的高位也做了运算(与高16位做异或运算,使得在做&运算时,此时的低位实际上是高位与低位的结合),这就增加了随机性,减少了碰撞冲突的可能性!
下面我们再来看看流程是怎么样的:
![a5f4046ee0fa66b7dffe7931b744df90.png](https://i-blog.csdnimg.cn/blog_migrate/160f014aa78e8390b7cb0c29153b1137.jpeg)
新值覆盖旧值,返回旧值测试:
![0a3dff4a4f41bbd60ef696deccaf0d6f.png](https://i-blog.csdnimg.cn/blog_migrate/f52742ea3b7b9e3471c75f83fe60fa16.jpeg)
接下来我们看看resize()
方法,在初始化的时候要调用这个方法,当散列表元素大于capacity * load factor
的时候也是调用resize()
![1429ed721b6fa314c01f8e1a4b8bad47.png](https://i-blog.csdnimg.cn/blog_migrate/5aa1f76a1329221b67ea506c85f2a7c6.jpeg)
1.3get方法
![afe74ea610c800206e7f5e82cc1746b1.png](https://i-blog.csdnimg.cn/blog_migrate/0bc0fe68d537b19f996c3a91610c7e2b.jpeg)
接下来我们看看getNode()
是怎么实现的:
![eadc8d6c501e4fc24c94f990df7615fa.png](https://i-blog.csdnimg.cn/blog_migrate/2edeaf8a948b7d26eb3209d12a47280b.jpeg)
1.4remove方法
![60b8eba2907014d362c28fa985011c7a.png](https://i-blog.csdnimg.cn/blog_migrate/5f4da305413ab01f2a613603bc9205fe.jpeg)
再来看看removeNode()
的实现:
![8caa962b2f987cfe4c30b8d013232de9.png](https://i-blog.csdnimg.cn/blog_migrate/8f5dc1cb170337aa98bfd04fb0b1123a.jpeg)
二、HashMap与Hashtable对比
从存储结构和实现来讲基本上都是相同的。它和HashMap的最大的不同是它是线程安全的,另外它不允许key和value为null。Hashtable是个过时的集合类,不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换
![6f39656b1efddffc1e136fcf8efd1dd3.png](https://i-blog.csdnimg.cn/blog_migrate/04a0658acd7aef63b940a09e0f873141.jpeg)
Hashtable具体阅读源码可参考:
https://blog.csdn.net/panweiwei1994/article/details/77427010
https://blog.csdn.net/panweiwei1994/article/details/77428710
四、总结
在JDK8中HashMap的底层是:数组+链表(散列表)+红黑树
在散列表中有装载因子这么一个属性,当装载因子*初始容量小于散列表元素时,该散列表会再散列,扩容2倍!
装载因子的默认值是0.75,无论是初始大了还是初始小了对我们HashMap的性能都不好
装载因子初始值大了,可以减少散列表再散列(扩容的次数),但同时会导致散列冲突的可能性变大(散列冲突也是耗性能的一个操作,要得操作链表(红黑树)!
装载因子初始值小了,可以减小散列冲突的可能性,但同时扩容的次数可能就会变多!
初始容量的默认值是16,它也一样,无论初始大了还是小了,对我们的HashMap都是有影响的:
初始容量过大,那么遍历时我们的速度就会受影响~
初始容量过小,散列表再散列(扩容的次数)可能就变得多,扩容也是一件非常耗费性能的一件事~
从源码上我们可以发现:HashMap并不是直接拿key的哈希值来用的,它会将key的哈希值的高16位进行异或操作,使得我们将元素放入哈希表的时候增加了一定的随机性。
还要值得注意的是:并不是桶子上有8位元素的时候它就能变成红黑树,它得同时满足我们的散列表容量大于64才行的~
![6349bf08e6398e046095152be0367faa.png](https://i-blog.csdnimg.cn/blog_migrate/c4faf8082b0e67621459d96582ea872d.jpeg)
![85de1a296ab5645b1e36fe37dc45f78e.png](https://i-blog.csdnimg.cn/blog_migrate/ee6a9ef79b97c2b1f5fdbd33031538a7.jpeg)
参考资料:
《Core Java》
https://blog.csdn.net/panweiwei1994/article/details/76555359
https://zhuanlan.zhihu.com/p/28216267
https://blog.csdn.net/fan2012huan/article/details/51097331
https://www.cnblogs.com/chinajava/p/5808416.html
ps:文字版的学不过瘾?往下看,小慕来给大家送福利啦,超值实战,仅需一元哦,没有套路~一块钱让你get到源码大师的编程内功和编程思想!
/ 推荐一门超值HashMap源码课程 /
![39a0d43c2e8114027f97d997337ce054.png](https://i-blog.csdnimg.cn/blog_migrate/70625e66055640446d5057ff2974b0cb.jpeg)
![f9e61a78ae099eccd00208cbfacc2379.png](https://i-blog.csdnimg.cn/blog_migrate/a654b4534718e6f52a2cb2e4b0fa9f98.png)
![6c750e6ade64e47b79c50a1be7d18ca2.png](https://i-blog.csdnimg.cn/blog_migrate/15beff6672dcfd20d2f47269af5646a7.jpeg)
END
了解课程详情,请点击原文阅读~
![e79b90eb62f2dfd1021a2a645c85c561.gif](https://i-blog.csdnimg.cn/blog_migrate/d9487c643c45f29634399c7119aa5634.gif)
● 盘点那些改变世界的代码!
● 程序员一般通过什么途径接私活?
● 我偷窥了XX个程序员的微信群聊,简直不忍直视····
● 用好这42款Chrome插件,每年轻松给你省出一个年假!
Tips:
# 点下“在看”❤️
# 然后,留个言踩个楼?每周都有不定时福利哦