(注意:本文源码基于JDK1.8)
前言
HashMap中有多达7处通过调用resize()方法进行扩容操作,接下来我们一起分析用于扩充数组容量的resize()方法,学习它的实现思想……加油……
resize()方法分析
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
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)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
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;
}
}
}
}
}
return newTab;
}
用于将HashMap对象持有的数组对象容量进行扩容的resize()方法,该方法会返回一个新的数组对象,HashMap对象会持有这个新的数组对象,且新的数组对象中会包含旧数组对象中保存的所有元素,以此完成扩容数组容量的目的,resize()方法虽然比较长,但是我们可以将其分解成一个一个的步骤,接下来我们一起对resize()方法的源码进行详细的分析……
一、准备阶段
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
1、创建局部变量用于存储HashMap对象持有的当前数组对象
实例变量table是HashMap对象持有的底层数组对象,此时赋值给创建的局部变量oldTab负责保存,相对即将用于扩容而创建的新数组对象而言,我们称它为旧数组
2、创建局部变量用于保存旧数组对象的容量
判断局部变量oldTab是否为null,如果为null,说明底层数组对象并没有创建过,此时为局部变量oldCap赋值为0。当底层数组对象已经创建,oldTab则不为null,获取旧数组对象的容量,并赋值给局部变量oldCap
3、创建局部变量用于存储扩容阈值
HashMap对象持有的实例变量threshold表示底层数组是否需要扩容的阈值,当数组中添加的元素总数大于threshold的值时,当前数组对象就需要进行扩容,此处将threshold赋值给局部变量oldThr保存,保存下旧数组的扩容阈值
4、创建两个局部变量,用于保存新的数组对象长度、以及新的扩容阈值
创建的两个局部变量,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)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
在if-else的选择分支语句下,对三种情况分别作出处理:
第一种情况:当前数组对象的容量oldCap大于0
oldCap是HashMap对象持有的当前数组对象的容量
1、针对旧数组容量oldCap大于常量MAXIMUM_CAPACITY的情况
将扩容阈值threshold赋值为Integer的最大值MAX_VALUE,然后直接返回旧数组的容量,这种情况已经无法再扩充数组容量了
2、正常扩大两倍容量的情况
先将旧数组容量oldCap左移1位,获得的容量值为原先的2倍
将2倍的数组容量赋值给newCap
对newCap的值进行判断,第一个条件是检查是否小于MAXIMUM_CAPACITY、第二个条件是检查旧数组容量oldCap是否大于等于默认容量DEFAULT_INITIAL_CAPACITY(默认容量为16)
第一和第二,两个条件都满足的情况下,使用旧数组阈值oldThr左移1位,得到双倍数赋值给新数组的扩容阈值newThr
第二种情况:数组对象的容量等于0且扩容阈值大于0
将旧数组的扩容阈值oldThr作为新数组newCap的容量,下面这行代码是HashMap对象创建时,做的threshold的初始化工作,在这里终于用到了threshold值
第三种情况:旧数组对象的容量等于0且旧数组对象扩容阈值等于0的情况,初始阈值为零表示使用默认值(说明HashMap对象的创建方式一定有多种)
使用默认容量DEFAULT_INITIAL_CAPACITY作为新数组的容量,将其赋值为newCap的值
使用默认加载因子DEFAULT_LOAD_FACTOR与默认容量DEFAULT_INITIAL_CAPACITY相乘,计算得出新数组容量的阈值,并赋值给newThr
三、处理新数组的扩容阈值为0时的情况,再次计算出新的阈值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
创建局部变量ft,用于存储计算出来的扩容阈值,新数组容量newCap与HashMap对象持有的loadFactor相乘,赋值给ft
新数组容量newCap小于最大容量MAXIMUM_CAPACITY、ft小于最大容量MAXIMUM_CAPACITY的情况,ft赋值给新的数组阈值newThr,反之为newThr赋值为整型最大值MAX_VALUE
四、将局部变量的值全部赋值到HashMap对象持有的扩容阈值threshold、持有的底层数组table
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
1、将计算好的扩容阈值进行赋值
为HashMap对象持有threshold进行赋值,局部变量newThr赋值给threshold
2、创建一个指定容量的Node数组对象
创建好的Node数组对象赋值给局部变量newTab
3、将创建的数组对象进行赋值
为HashMap对象持有的实例变量table进行赋值,局部变量newTab赋值给table
五、遍历旧数组中的每一个桶(下标),并将旧数组中的元素全部挪到新的数组对象中
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;
}
}
}
}
}
对旧数组对象中每一个下标处(桶处)可能出现的三种情况分别进行处理
1、桶内一个元素
2、桶内是红黑树
3、桶内是单链表
针对不同的情况,使用不同的处理方式,一起学习一下
第1行:判断旧数组对象是否存在,oldTab不为null表示存在旧数组
第2行:当旧数组存在时,开始遍历旧数组,起始下标j为0,结束遍历的条件为下标j等于旧数组容量oldCap时(意味着旧数组中的每一个元素都会被遍历)
第3行:先定义一个临时变量e,用于存储旧数组的每个桶时获取到的Node对象
第4行:从旧数组的桶下标j处取出一个Node对象,将Node对象赋值给临时变量e,接着马上判断e是否指向一个存在的Node对象,若不存在,即e==null,则for循环里面后面的语句都不会再执行,接着继续j+1,开始取出底层数组下一个桶中的Node对象
第5行:第4行为true时执行,因为旧数组下标j处的Node对象已经被临时变量e持有了,所以这里为旧数组oldTab的下标处赋值null,把该强引用断开,后面便于GC回收
第6-10+行:这里就是精彩的代码部分了,对于从旧数组某一个下标j取出来的Node对象,取出Node.next的值,然后马上判断Node.next的值,这里对象3种情况分别进行处理
第一种情况是next == null 代表该桶只有一个Node
第二、三种情况是若next不为null,桶内持有的可能是一个红黑树结构(e instanceof TreeNode)、或者是单链表结构;
红黑树结构,则会进一步调用TreeNode的split()方法,split接受4个参数,依次代表:当前HashMap对象、新数组对象、下标j、旧数组对象);
单链表结构则会将原来的单链表有可能分割成两个单链表,一个记为低位链表loHead、另一个记为高位链表hiHead,最后分别插入到原数组的j下标处、以及j+oldCap处,这个写的太精妙了,并没有为单链表中的每一个元素重新进行rehash的计算!而且大佬很重视细节,loTail.next和hiTail.next均最后还又赋值为null,因为取出来的元素,很可能原来的next值是指向下一个元素的,这样就能保证形成的两个单链表,最后一个元素的next都指向为null!
六、返回创建的新数组对象
return newTab;
细节1:红黑树结构TreeNode的split()方法
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
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) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
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);
}
}
}
用于将红黑树结构转换为单链表结构的方法,阈值是6,为啥是6?红黑树的分析得单独开文章了,太复杂了……
细节2:单链表的遍历
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;
}
将旧数组对象中的桶中的元素挪到新的数组对象中……,如果桶中的数据结构是单链表,则将单链表分成两个单链表,分别存储到新数组对象中的两个桶中,作者是怎么想到的?真牛逼(桶中持有单链表的头结点即可,竟然还做一个拆分)
总结
1、HashMap扩容机制,本质上是创建一个新的Node数组对象,然后再将旧的Node数组对象中的所有元素,全部挪到新的数组对象中
2、每个需要从旧数组对象中挪动的Node元素,并没有重新计算它持有的称为hash的值,而是使用Node对象持有的hash原值与新的数组容量进行计算,得到一个新的桶地址(哈希地址,数组下标)
3、当桶内是单链表结构时,可能会将单链表分成2个,然后再将2个单链表分别存储在新数组中的两个桶内(不同的下标处),一低一高(旧数组中的元素并不是一个元素一个元素的复制到新数组中的,而是只把单链表的头结点赋值过去即可)
4、为什么单链表转红黑树的阈值是8?为什么红黑树转单链表阈值则是6?
5、为什么单链表转红黑树,要加容量64的限制?
显然扩容篇没有学的很透彻……大佬太牛逼!!!!