向HashMap的put添加元素过量的时候,hash结构会退化成链表。所以会有一个扩容机制来解决这种问题,扩容的步骤就是新建一个大一倍的数组,将旧的数组里面的所有节点重新计算放到新的数组里面。
在jdk1.7中是重新计算节点新槽位的下标再插入,但是在1.8中是采用一种新的方式。
关键源码:
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 { //====》关键在此
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;
}
}
}
}
上述的最后else分支做的就是将原指定槽位的所有节点移到新的数组中。原则是在移动当前节点的时候,防止下一个节点丢失,要记录下一个节点;
Node<K,V> loHead = null, loTail = null;//低位链表的头、尾指针
Node<K,V> hiHead = null, hiTail = null;//高位链表的头、尾指针
也就是将原来的一条链表拆成两条链表,低位链表的数据将会到新数组的当前下标位置(原来下标多少,新下标就是多少),高位链表的数据将会到新数组的当前下标+当前数组长度的位置(原来下标多少,新下标就是多少+当前数组长度)。
计算新的槽位下标是看当前hash与旧数组长度相与,结果为0的话那么新槽位下标还是当前的下标,如果非零,那么新槽位下标是当前下标+当前数组长度。举个?:hash为1,当前数组长度为8,1&8 为 0,所以下一个槽位就是1;hash为9,当前数组长度为8,9&8 不为 0,所以下一个槽位就是1+8 = 9。
为什么可以这样,难道说原来在一个槽位的所有数据在新数组中就最多只能分到两个槽位吗?事实证明,是的!
看看HashMap的put操作:
算法1.计算hash:(h = key.hashCode()) ^ (h >>> 16),自己与自己的高16位异或
算法2.计算槽位:(tab.length - 1) & hash,hash与数组长度-1相与
可以看出第一步采用的是死算法,计算的结果为固定结果,对长度不同的数组插入位置不会造成影响,这里忽略。造成影响的是第二个步骤:通过算出的固定结果与不同长度的数组相与的结果会有差异,这就会造成不同的数据在不同长度的数组中保存的下标会不同。
观察一下,上述分槽用的算法是当前hash与长度相与,如果为0,那么新槽位不变。否则新槽位为原来数组长度+当前下标;让事实验证一下:
1. key1算出来的hash为1(0001),当前数组长度为8,计算槽位:
通过算法2:1&7 ==> 001&111 ==> 1,所以在1号槽位
扩容后当前数组长度为16,重新计算:
通过算法2:1&15 ==> 0001&1111 ==> 1,所以在1号槽位
2. key2算出来的hash为9(1001),当前数组长度为8,计算槽位:
通过算法2:1&7 ==> 1001&111 ==> 1,所以在1号槽位
扩容后当前数组长度为16,重新计算:
通过算法2:1&15 ==> 1001&1111 ==> 1001,所以在9号槽位。
结果显示还确实是这样,分析一下:从计算槽位的算法可以看出,能在一个槽位的所有数据,它们hash低k位都是相等的,k为当前数组长度-1的二进制位数。比如:hash为1的key1和hash为9的key2在数组长度为8中是一个槽,那么key1和key9的hash的低3位(数组长度为8,8-1=7,7二进制有3位)都是一样的。
那么当数组扩容,长度会加倍。那么重新计算方式唯一会造成差异的就是hash的倒数第k+1位。
因为原来是hash&(111)7,k为3,那么现在算法是hash&(1111)15,k为4了。
所以造成差异的就是看倒数第k+1位是不是0,如果是0,那么第二次计算的结果不会变化:如001&111和001&1111与是一样的结果,所以新槽位不会变,但9就不同了,1001&111 和 1001&1111结果会差个值,那个值就是倒数第k+1位所代表的十进制值(倒数第k+1=4,对应二进制1000,十进制8),也就是原来的数组长度,新槽位就为当前槽位+原来数组值。