通过优锐课核心java学习笔记中,我们可以看到,码了很多专业的相关知识, 分享给大家参考学习。
HashMap的底层数据结构是数组+链表+红黑树,我们知道数组的长度是固定的,所以涉及到扩容的概念,在HashMap中resize()方法就是完成这项工作的。
resize()方法有两个主要的作用:
1:初始化底层数组table
2:进行扩容
接下来我们从源码角度分析以下:
第一段:映入眼帘的是获取当前的数组和扩容的阀门
//把当前底层数组赋值给oldTab
Node<K,V>[] oldTab = table;
//获取当前数组的大小oldCap
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//获取扩容的阀门
int oldThr = threshold;
int newCap, newThr = 0;
第二段:映入眼帘的是条件判断语句,判断是初始化数组还是扩容。
if (oldCap > 0) {
//走到这一步:说明数组已经被初始化了
if (oldCap >= MAXIMUM_CAPACITY) {
//走到这一步:说明底层数组的长度已经是最大的了。
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//走到这一步:说明数组的长度扩展到原来的2倍,阀门threshold扩展到原来的2倍。
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//走到这一步:说明调用HashMap的有参构造函数,因为无参构造函数并没有对threshold进行初始化。
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//走到这一步:说明调用的无参构造函数
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
对上面的第二段代码进行总结如下:
1:如果已经对底层数组进行了初始化,则进行扩容,
1.1:如果当前数组的长度已经最大的整数值了,就最大值赋值给threshold.
1.2:如果没有达到最大的整数值,则把数组长度扩展到原来的两倍,把阀门threshold扩展到原来的两倍
2:如果调用了HashMap(int initialCapacity, float loadFactor)或者HashMap(int initialCapacity),这两个构造函数都会初始化threshold,所以oldThr=threshold>0,会进入此条件,初始化数组长度newCap=oldThr=threshold
3:如果调用了HashMap(),此时并没有初始化threshold,所以会进入else.初始化数组长度为16,阀门为16*0.75=12.
所以第二段的条件判断语句就出现了resize()方法的两个重要特性初始化数组和扩容机制。
第三段代码:把上面的新的数组和新的阀门进行赋值给HashMap的全局变量
//把上面条件语句计算的newThr赋值给阀门。
threshold = newThr;
@SuppressWarnings({“rawtypes”,“unchecked”})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//把新的数组赋值给底层数组table
table = newTab;
第四层:如果是扩容,则执行下面的代码,如果是初始化,则resize()方法结束。
if (oldTab != null) {
//走到这一步:说明是扩容,上面已经完成了扩容,重新创建了底层数组,所以需要把老的数据迁移到新的数组中。
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
//进入这一步:说明此下标有元素。
oldTab[j] = null;
if (e.next == null)
//进入这一步:说明此下标就一个元素。
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果这一步:说明此下标是一颗红黑树,所以对红黑树进行迁移
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//进入这一步:说明此下标是一个链表,所以对链表进行迁移
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
老数据的迁移也是一个条件语句,主要分3个不同的条件。因为是互斥条件,所以每一个老的元素只能进入3个条件之一:
第一个条件:如果指定下标就一个元素
这种情况下是最简单的,直接通过(newCap-1)&hash找到需要放入的新下标,然后直接放入即可。代码体现:
if (e.next == null)
//进入这一步:说明此下标就一个元素。
newTab[e.hash & (newCap - 1)] = e;
补充说明:在分析第二个条件和第三个条件之前,我们来说一个小小的概念,也是JDK8以后的奥妙之处。
例如HashMap默认的数组长度为16,通过扩容有变成原来的2倍,变成32.计算一个元素的下标:(n-1)&hash
举例说明:
默认数组的长度n=tab.length=16.HashMap中分别有3个元素,key的hash分别如下:
k1.hash=3:二进制表示:00000011
k2.hash=19:二进制表示:00010011
k3.hash=128:二进制表示:10100011
扩容前n=tab.length=16,那么3个元素的下标会怎样?
通过扩容后n=tab.length=32,那么3个元素的下标会怎样?
扩容前:通过(n-1)&hash获得下标如图所示:
带你走进Java集合-HashMap的扩容机制-resize()
扩容后:通过(n-1)&hash获得下标如图所示:
带你走进Java集合-HashMap的扩容机制-resize()
通过扩容前和扩容后进行比较,大家发现了什么变化吗?现在我来总结以下
1:扩容后比扩容前的二进制多了一个高位为1,我们把这个高位用一个变量q表示。
16-1:00001111
32-1:00011111
2:如果元素的hash值的二进制q位是0的话,它的下标就不会改变
3:如果元素的hash值的二进制q位是1的话,它的下标就等于在原来下标基础上加上扩容前的长度(index=oldIndex+16)
我们知道了这个规律,那么我们怎样才能知道那一个高位是0,还是1呢?不能每次都把hash值变成二进制数一下吧,所以HashMap给出的获取那个高位的方法如下:
解决方案:e.hash&oldCap
1)e.hash:表示当前元素的hash
2)oldCap:表示扩容前数组的长度
因为oldCap一定是2的n次幂,所以只有高位是1,其余是0,如果e.hash的高位0,则e.hash&oldCap=0,如果e.hash的高位是1,则e.hash&oldCap=1.
这一个知识点如果大家理解了,下面第二个条件和第三个条件的代码就非常容易理解了,那么HashMap为什么会这样操作呢?我们知道在JDK8以前,扩容后迁移元素会重新计算hash的,如果是链表,则先把链表的尾节点迁移到链表的首节点,这样在并发时,很容易出现死循环。所以JDK8以后,就对这种扩容机制做了改变,省去了重新hash的时间,也不会因为并发造成死循环。
第二个条件:如果指定的下标是一颗红黑树
这种情况下就是对红黑树的迁移了,这个方法并没有直接操作,而是调用了红黑树的split方法对此条件进行处理。接下来我们进入红黑树的split方法进行分析。
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
//定义两个红黑树
//如果此元素那一个高位是0,则放在loHead中
//如果此元素那一个高位是1,则放在hiHead中
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
//进入这一步:说明那一个高位是0
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
//进入这一步:说明那一个高位是1
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
//进入这一步:说明需要把红黑树在变回成链表
tab[index] = loHead.untreeify(map);
else {
//进入这一步:说明下标不变,仍放到原来的位置
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
//进入这一步:说明需要把红黑树在变回成链表
tab[index + bit] = hiHead.untreeify(map);
else {
//进入这一步:这些元素的下标改变,新的下标=原来下标+扩容前数组长度。
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
第三个条件:如果指定下标是一个链表
代码如下:
//定义两个链表,
//如果那一个高位是0,则放在loHead中。
//如果那一个高位是1,则放在hiHead中。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//进入这一步:说明此时元素那一个高位是0,放在loHead链表中
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
//进入这一步:说明此时元素那一个高位是1,放在hiHead链表中
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
//进入这一步:这些元素的下标不变,仍放在原来下标
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
//进入这一步:这些元素的下标改变,新的下标=原来下标+扩容前数组长度。
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
通过上面的分析,HashMap的扩容也不是那么的复杂,只要理解了上面补充说明的分析,相信大家很容易理解。总结如下:
1:初始化数组
2:扩容
3:理解e.hash&oldCap的意义。
喜欢这篇文章的可以点个赞,欢迎大家留言评论,记得关注我,每天持续更新技术干货、职场趣事、海量面试资料等等
如果你对java技术很感兴趣也可以加入我的java学习群 V–(ddmsiqi)来交流学习,里面都是同行,验证【CSDN2】有资源共享。
不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代