HashMap扩容机制
前情回顾:谈谈jdk1.8的HashMap 01
上次我们把put方法讲完了,有一个方法我没有细说,那就是resize()方法,这个方法我个人认为是HashMap最精华但是却最难理解的地方,接下来我们慢慢讲解。
回顾一下成员变量
transient Node<K,V>[] table;//装Node的数组
transient int size; //HashMap的大小
int threshold; //容量
final float loadFactor;//负载因子
resize()方法
resize()方法的作用就是扩容还有初始化。
在该方法中,我们要着重注意old/newCap(旧/新数组长度)和old/newThr(旧/新容量),
看看代码吧,部分解析已经写在了注释里:
//当第一次调用这个方法的是时候,oldCap和oldThr都为0,
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;
}
//若执行到这,则是初始化
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//如果能执行到这里,也就是说调用的是有参构造,oldThr必定是2的整数次幂
else {
//运行到这里就表示是调用了无参构造,进行初始化
//16
newCap = DEFAULT_INITIAL_CAPACITY;
//12
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) {
//这里我个人认为是方便GC
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;
}
新旧数组数据迁移
在jdk1.7中,每一个节点都要先经过一个扰动函数的计算后再重新计算下标,而在1.8中,是这么进行的…
如果没有hash冲突,也就是说某节点没有next节点,那就直接和新数组长度取余获得下标,如果有hash冲突,那么就开始一个循环:首先定义四个Node类型变量:
loHead,loTail,hiHead,HiTail(其实就是low和high的缩写),然后遍历该链表,把每一个结点的key的hash值与旧数组长度(假设是2的n次方)相与,目的是为了得到第n位的数是是否为0,如果是0,则把该结点放入以loHead为头,以loTail为尾的链表中,反之,则放入以hiHead为头,以hiHead为尾的链表。过程如下:
hash: 0000 0000 1110 1111
oldCap:0000 0000 0000 1000
相与的结果不是0,因此现在high链表中只有一个结点,接着遍历该结点的next结点,假设
hash: 0000 0000 1110 0111
oldCap:0000 0000 0000 1000
相与的结果是0,那么把该节点放入low链表中,接着遍历下一个结点,同样执行上述操作,如果结果是1,则放入high链表的尾部,重复上述过程直到next结点为null。
最后把low链表放到原位置,把high链表放到原位置+旧数组长度的位置,假设,Node数组长度为16,现在遍历到oldTable[2],发现table[2]发生了哈希冲突,有Node1<1,a>,Node2<2,b>,Node3<3,c>,经过上述操作后,low链表有Node2和Node3,而high链表有Node1,则最后新数组是这样放的,在newTable[2]的地方放入Node2(next结点就是Node3),在newTable[2+16] 的地方放入Node1,结束。
画个图加深理解吧
经过上述运算后
最后
其他
其实HashMap体现了一种懒加载的思想,当我们调用完构造方法后,成员变量里的table是还没有被赋值的,也就是说这个时候table=null,直到第一次执行put方法时才会进行初始化。