HashMap扩容方法resize()源码:
//HashMap允许的最大容量,我理解就是数组的最大长度,而不是键值对总数
static final int MAXIMUM_CAPACITY = 1 << 30;
//数组默认初始长度
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
final Node<K,V>[] resize() {
//把当前HashMap的数组赋值给oldTab,顾名思义,oldTab就是老数组了
Node<K,V>[] oldTab = table;
//取得老数组的长度,为null说明HashMap还没完成初始化,返回0,否则返回数组长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//把当前HashMap的阈值赋值给oldThr,顾名思义,这个oldThr就是老阈值了
int oldThr = threshold;
//定义新的数组长度newCap,新的阈值newThr,默认为0
int newCap, newThr = 0;
//如果oldCap大于0,说明老数组至少已经初始化完成了
if (oldCap > 0) {
//如果老数组的长度oldCap大于等于MAXIMUM_CAPACITY,说明数组长度已经超过最大限制2的30次方了
if (oldCap >= MAXIMUM_CAPACITY) {
//就不会再扩大数组了,直接把Integer.MAX_VALUE赋值给阈值,Integer.MAX_VALUE=2^31-1
threshold = Integer.MAX_VALUE;
//然后返回旧数组,这步可以理解成数组已经无法再扩大了,只能扩大能容纳的键值对总数了
return oldTab;
}
//否则就把旧的数组长度oldCap左移一位,也就是乘以2赋值给新数组长度newCap,同时还要判断新数组长度newCap要小于MAXIMUM_CAPACITY以及旧的数组长度oldCap不能比默认初始数组长度DEFAULT_INITIAL_CAPACITY(16)小
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//同时旧的阈值也要乘以2,然后赋值给新阈值
newThr = oldThr << 1; // double threshold
}
//如果老数组的长度oldCap并没有大于0,说明还没做初始化操作,但是这时它的旧阈值oldThr却大于0,这说明了构造HashMap时传入了初始容量,而HashMap会根据传入的初始容量来定义阈值,这里给出部分代码参考:this.threshold = tableSizeFor(initialCapacity),而数组的初始化其实是在put()方法里才完成的,这也就能理解为什么有阈值而数组却还没初始化了
else if (oldThr > 0)
//那就把阈值值赋值给代表新数组长度的变量newCap
newCap = oldThr;
//走到这步,就说明旧阈值和旧数组都还没做初始化,说明调用的是HashMap无参的构造函数
else {
//初始化新数组长度newCap为DEFAULT_INITIAL_CAPACITY(16)
newCap = DEFAULT_INITIAL_CAPACITY;
//初始化新阈值newThr为(加载因子*默认容量)=0.75*16=12
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
//如果新阈值等于0,其实可以发现上面大部分判断的代码块里都有设置newThr的值,只有else if (oldThr > 0){}这个判断里没有设置newThr的值,其他的要么早就有值,要么就做设值操作
if (newThr == 0) {
//新数组长度newCap*负载因子loadFactor的值赋值给ft
float ft = (float)newCap * loadFactor;
//然后判断下新数组长度newCap是否超过最大容量,同时ft的值如果超过MAXIMUM_CAPACITY,则设置为Integer.MAX_VALUE,否则返回ft
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
//把新阈值newThr赋值给HashMap里代表阈值的属性字段threshold
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//创建一个长度为newCap的新数组,可以理解成,如果是在做初始化数组操作的话,那这就是初始化的数组,如果是在做扩容操作的话,那这就是新数组,是要用来扩容的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
//把新数组赋值给HashMap里代表数组的属性字段table
table = newTab;
//如果旧数组不是null,说明要扩容,那接下来就要把旧数据移动到新数组里了
if (oldTab != null) {
//开始循环旧数组,oldCap是旧数组长度
for (int j = 0; j < oldCap; ++j) {
//定义一个变量e
Node<K,V> e;
//取得当前下标的数组节点,赋值给e变量,并判断是否为null
if ((e = oldTab[j]) != null) {
//不为null,说明旧数组在这个下标里有值,先把旧数组的这个下标位置的引用设置为null,方便GC时回收
oldTab[j] = null;
//如果e的后继节点为null,说明还没拉出链表来,这个桶里就一个节点
if (e.next == null)
//那就通过[e.hash & (newCap - 1)]计算出对应的新数组下标位置,并赋值上e节点,其实[e.hash & (newCap - 1)]操作就是对newCap做取余操作,只是用按位与运算效率会高很多
newTab[e.hash & (newCap - 1)] = e;
//如果e节点是TreeNode类型,说明结构已经是红黑树了
else if (e instanceof TreeNode)
//那就调用split()方法做扩容
((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类型的变量next
Node<K,V> next;
do {
//取e的后继节点赋值给e变量
next = e.next;
//这里其实就是做一个按位与运算,具体怎么算,下面我会给个🌰
if ((e.hash & oldCap) == 0) {
//如果低位尾节点为null的话,说明还没开始遍历这个桶下的链表,就把e赋值给低位首节点
if (loTail == null)
loHead = e;
//否则低位尾节点不为null的话,说明已经在遍历了
else
//把低位尾节点的后继节点设置为e节点
loTail.next = e;
//把e节点赋值给低位尾节点,因为每次e节点都会被赋值成next,而原来的e又被赋值成loTail,通过loTail.next = e,就可以让e的后继节点指向e.next,所以这步加上上一步,就可以形成一个单向链表了
loTail = e;
}
//如果e节点的hash值对oldCap取余不等于0,说明这个节点是下标0之外的数组节点
else {
//这时判断高位尾节点是否为null
if (hiTail == null)
//如果高位尾节点为null的话,说明还没开始遍历这个桶下链表,就把e赋值给高位首节点
hiHead = e;
else
//如果高位尾节点不为null的话,说明已经在遍历了
hiTail.next = e;
//把e节点赋值给高位尾节点,因为每次e节点都会被赋值成next,而原来的e又被赋值成hiTail,通过hiTail.next = e,就可以让e的后继节点指向e.next,所以这步加上上一步,就可以形成一个单向链表了
hiTail = e;
}
//这里的next=e.next,循环直到没有下一节点为止
} while ((e = next) != null);
//如果低位尾节点不为null
if (loTail != null) {
//这个低位尾节点也没有后继节点了
loTail.next = null;
//就把首节点赋值给新数组下标为j的桶,和旧数组的位置是一样的,也就是说节点原来对应在旧数组的哪个下标,在新数组也不变。这里说点抽象的,把首节点赋值给新数组的桶,其实不单单只是首节点,因为每个节点都会有指针指向后继节点,所以其实可以想成是直接拉了一个链表到这个数组的某个桶里了。
newTab[j] = loHead;
}
//如果高位尾节点不为null
if (hiTail != null) {
//这个高位尾节点也没有后继节点了
hiTail.next = null;
//就把首节点赋值给新数组下标为【j+oldCap】的桶,和旧数组的位置j相比,这里多偏移了旧数组长度oldCap个位置,变成了【j+oldCap】
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
e.hash & oldCap的🌰
首先要知道oldCap的长度都是2的次幂,比如16,32,转换成二进制的话,它的有效最高位是1,低位都是0,拿16做个🌰:
0000 0000 0000 0000 0000 0000 0001 0000
如果oldCap都是这样只有有效最高位是1,其余都是0的二进制的话,那么其实e.hash的二进制真正能够参与到运算的有效位数就是oldCap的最高位到最低位的位数,比如oldCap是16的话,那e.hash的真正有效位数就是5位
下面来做个按位与运算,只有相同的二进制数位上,都为1才是1,否则都是0:
1111 1010 0000 1111 0000 1111 1100 1111 ---->e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 0000 -----16的二进制
0000 0000 0000 0000 0000 0000 0000 0000 —>0
再换个e.hash
1111 1010 0000 1111 0000 1111 1101 1111 ---->第二个e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 0000 -----16的二进制
0000 0000 0000 0000 0000 0000 0001 0000 ---->16
通过以上两个运算可以发现,只有e.hash与oldCap对应的有效高位上的值是1,运算结果才不为0,否则都是0。
数组长度之前为16,所以看下以上的两个e.hash对数组长度取模e.hash & (16-1)获取索引的结果是多少:
1111 1010 0000 1111 0000 1111 1100 1111 ---->e.hash的二进制
0000 0000 0000 0000 0000 0000 0000 1111 -----(16-1)的二进制
0000 0000 0000 0000 0000 0000 0000 1111 ---->15
1111 1010 0000 1111 0000 1111 1101 1111 ---->第二个e.hash的二进制
0000 0000 0000 0000 0000 0000 0000 1111 -----(16-1)的二进制
0000 0000 0000 0000 0000 0000 0000 1111 ----15
这是假设数组长度已经扩容成了32,把以上的两个e.hash对数组长度取模e.hash & (32-1))获取索引的结果是多少:
1111 1010 0000 1111 0000 1111 1100 1111 ---->e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 1111 -----(32-1)的二进制
0000 0000 0000 0000 0000 0000 0000 1111 ---->15
1111 1010 0000 1111 0000 1111 1101 1111 ---->第二个e.hash的二进制
0000 0000 0000 0000 0000 0000 0001 1111 -----(32-1)的二进制
0000 0000 0000 0000 0000 0000 00001 1111 ----> 31
通过上述运算就可以发现,e.hash与oldCap对应的有效高位上的值是0的话,哪怕扩容了,新数组索引还是不变,还是15,而如果e.hash与oldCap对应的有效高位上的值是1的话,那这个元素在新数组的下标位置就等于【原数组下标位置+原数组长度】=【15+16】=31。这样就通过这个运算把原来的一条链表拆成了两条链表,然后这两条链表各自归属到新数组中对应的位置。
总结
阅读到这,已经能够大致了解HashMap的扩容机制了,可以发现JDK1.8没有再像JDK1.8前一样再重新rehash了,效率自然是提高了很多。