HashMap底层详讲

【JDK1.7】

底层数据结构:

存储:hashMap存储的是键值对,允许key为null,也允许value为null。

内部:位桶数组+ 链表

特点:同一hash值的链表都存储在一个链表里,当位于一个桶中的元素较多,即发生hash冲突比较多时,HashMap会将同一个桶中的数据以链表的形式存储,通过key值依次查找的效率较低。

【JDK1.8】

底层数据结构:

存储:hashMap存储的是键值对,允许key为null,也允许value为null。

内部:位桶数组 + 链表 + 红黑树

特点:同一hash值的链表都存储在一个链表里,当位于一个桶中的元素较多,HashMap会将同一个桶中的数据以链表的形式存储,如果链表长度到达阀值(默认是8),就会将链表转换为红黑树。

注意:

1、 创建一个Map时,并不会初始化位桶数组。

 

只会计算出根据初始容量(这里是12)计算出 threshold 为16。

由于tableSizeFor(initialCapacity)方法

        

 

不管你传的初始容量值是9、10、11、12、13…16,其计算出来n+1永远为16(2的n次幂)

所以,tableSizeFor(initialCapacity)方法的真实目的是为了确保所有map初始化时的容量均为2的n次幂。

2、 第一次put时,会调用到putVal方法,此时的threshold 早已经在我们new HashMap的时候计算出并赋值为为16。

        1.检查table是否为空,table的长度是否为0

        2.只要满足其中一条,就会首先对table进行一次resize()扩容(这也是集合的首次扩容)

     

        ​​​​

         如源码所示,初始容量会由threshold去初始化。所以可以看到,初始化给的12,而真正初始化的map容量大小其实是2的n次幂(这里是16)。

而真正的阈值是在699行这里进行的,可以看到其阈值最大值不能超过Integer.MAX_VALUE,

而容量的阈值不能大于MAXIMUM_CAPACITY(1 << 30)即2的30次幂:

 

 好,现在开始计算插入的元素应该在位桶的哪个位置,如下,看代码632行

 根据i = (n -1) & hash 算出其位置,n为容量16,hash值为49,代入算出i = 1,也就是位桶下标为1的位置,并在该位置创建一个节点Node。

继续调试,执行完放入元素之后modCount自增,size自增,并和扩容阈值(当前是12)比较,1小于12不用扩容

3、 当put一个key相同的键值对

         我们来分析其源码:

        此时,key的hash值相同,定位的位置已经有元素了,所以走了源码635-637行

由于目前元素没有过多,所以暂时并未红黑树化,所以跳过其他else直接来到654行进行值的覆盖。 

4、 为了触发扩容,将其一直put到”10”,发现”10”的hash值瞬间大到1567,那位置可能大概率就不连续了。

果然,以前通过i = (n -1) & hash算出来的下标位置都是连续的1~9,而key=”10” 通过i = (n -1) & hash计算出其下标位置变成15了!

put(“11”,”12”)计算出来的位桶数组位置为i = (n -1) & hash = 0位,所谓如图所示:

        观察源码663-664可知:

                当put第12位时仍不会触发扩容,第13位就会触发resize()扩容机制。

         

 从12位计算出位置为i = (n -1) & hash = 1,所以会放到下标为1的位置上边,但是该位置上边是有元素存在的,所以执行完源码的扩容方法之后会进来这里:

 在位于桶1的位置的元素节点的next位置,创建该节点接到后边。

        而下面这段代码其实回去从链表的初始节点一直判断其next节点是不是为null,如果next一直不为空直到++binCount >= 7,那么链表将会转化为红黑树。

 此时的hashMap结构为:

当key=”13”时,正常逻辑,其应该会接在位置2的后边,也就是这样:

 但是,此时size>12阈值了,即

 

所以会执行一次resize()!!!!也就是扩容!!!

执行的源码如下:

新的容量和新的阈值都翻了一倍,也就是newCap=32,newThr=24 

 扩容之后,就需要考虑元素迁移的事情了。

元素迁移:

首先是进到了旧的数组不为空的分支,开始元素迁移

看如下源码,我们来进行分析一下:

        

         首先,遍历的旧表的每个下标,首先是数组第0位,也就是         

                       

不为空,则先将旧表该位置的链表赋值给e,然后置为null等垃圾回收,

接着判断e有没有后驱节点next,没有的话就表示该下标下边只有一个元素,

开始元素迁移:

其位置为newTab[e.hash & (newCap - 1)],也就是newTab[1568 & 32- 1] = newTab[0],按位与运算结果为0,则迁移至newTab[0]

接着遍历数组下一个下标1,该下标为1也有两个元素,所以:

先将旧表该位置的链表赋值给e,然后置为null等垃圾回收,

接着判断e有没有后驱节点next, 此时该下标下有多个元素且少于8个,所以next不为空,

开始来到源码719行进行元素迁移:

 

        首先定义两个链表的头尾,一个链表用来装与旧表元素位置相同的元素,另一个链表用来装需要重新分配位置的元素。

现在我们知道下标为1的位置有两个元素

                                                        

        首先是当前链表key=”1”的元素e,其(e.hash & oldCap) = 49 & 16不满足==0的条件,所以其位置是需要移动的!即会执行 

 此时hiHead = Node(K=1,V=1,hash=49) ,同时链表尾部hiTail也指向e

接下来遍历下一个节点,并赋值给e

                                        

那么下一个元素会判断其(e.hash & oldCap) = 1569 & 16 = 0 刚好满足==0的条件

则会执行如下代码

此时loHead = Node(K=12,V=12,hash=1569),同时链表尾部loTail也指向e

后边无节点遍历,跳出do…while…

开始执行

                                

元素最终迁移的位置:

最后将loHead放在newTab[1]即在新数组中与旧数组位置相同的地方!!

而hiHead则被放在新的数组newTab[1 + 16]即在旧数组位置基础上再加上旧数组的容量!!

最后,简单画了一下HashMap的存储流程,太晚了就不优雅了,见笑

 

一篇博客肝了将近三个小时,创作不易,点个赞呗!

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值