【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的存储流程,太晚了就不优雅了,见笑
一篇博客肝了将近三个小时,创作不易,点个赞呗!