HashMap扩容方法源码
失踪人口回归!终于腾出手来发一篇水文,给大家乐呵乐呵。
无发方强!共勉
HashMap的扩容
当原数组存放元素过多会导致Hash碰撞的概率增大,降低效率。所以需要适时的对数组进行扩容,以减少Hash碰撞,存储更多的元素。默认扩容阈值是原数组空间利用0.75之后进行扩容。
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;
}
初次看到这样的代码我惊呼天人。这样的代码实在是太精巧了!!我们来解读一下代码
-
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); }
-
这段代码描述了旧数组如果有元素的话就继续判断元素数量是否超过最大限度
- 如果已经大于等于最大长度,则就无法扩容,直接返回旧数组就好
- 如果不大于最大长度,通过将旧数组的元素扩容阈值左移一位作为新的扩容阈值,也就是扩大二倍
-
如果旧数组元素长度不大于0(没元素的情况)并且旧数组扩容阈值>0,就将新数组长度作为旧数组的额扩容阈值
-
如果以上两种都不满足,则代表当前HashMap是空的,尚未创建任何Node表,就默认初始化一个Node表。可以看到初始化的Node表长度为16,扩容阈值为0.75*16=12
-
if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr;
-
当经过之前的if处理之后新数组扩容阈值仍未确定的情况下,确定一个扩容阈值
-
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);
-
如果oldTab(旧的Hash数组)不为空,则逐个遍历Hash数组,遇到不为空的元素,就判断该元素的next指针是否连接的有其他元素
-
如果没有连接其他元素,则直接在新Hash数组中根据hash算法为元素重新找一个位置
-
如果连接有其他元素 则判断该结点类型是否为TreeNode(红黑树结点),如果是红黑树结点 则执行树结点的切分方法
-
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; } }
-
else{} 代表该结点为一个普通的链表结点lohead -> 原hash链表的头 lotail ->原hash链表的尾 hihead -> 新hash链表的头 hitail->新hash链表的尾
-
e.hash & oldCap 是一段非常非常巧妙的代码
- 当e.hash & oldCap == 0 代表该元素在旧数组中的位置和新数组中的位置相同,所以不用进行移动
- e.hash & oldCap != 0 则代表该元素需要移动,并且 e.hash & oldCap的值就是该元素在新数组中相对于旧数组位置的偏移量
- 具体原因比较复杂:与位运算、移位等操作有关,可以参考:https://blog.csdn.net/u010425839/article/details/106620440
-
有了上面的预备知识,看上面的代码就很容易了
- 首先遍历该链表,判断每个元素是否需要移动,如果需要移动 则放入hihead链表中,否则放入lohead链表中
- 遍历结束后将两个链表分别放入新Hash数组的不同位置
resize() 总结:
HashMap的扩容方法大致分为两大步:
- 第一步对旧数组的参数进行检验并计算获取扩容后数组的参数信息
- 第二步根据不同的结点类型将元素放入新数组的适当位置
一个字:HashMap真妙(来自舔狗的跪舔)