HashMap作为一种集合,其容量自然不可能是无限的,既然容量有限,当数据存储到一定量的时候就会进行扩容,因此本篇文章对HashMap的扩容做一下源码简析,文章目录如下
一、源码简析
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 获取扩容前哈希桶的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 判断扩容前哈希桶长度是否大于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) {
// 老哈希桶从索引0开始向后遍历
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
// lo链表挂在当前索引位置
Node<K,V> loHead = null, loTail = null;
// hi链表挂在当前索引+原哈希桶长度的位置
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 用hash值和老哈希桶长度进行与运算来判断将节点放置在哪个链表上
if ((e.hash & oldCap) == 0) {
if (loTail == null)
// 如果lo链表尾节点为空,则将改节点设置为头节点
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;
// 将lo链表挂在当前索引位置
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
// 将hi链表挂在当前索引+原哈希桶长度的位置
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
扩容的过程大体可以分为两部分,以代码中的分隔线为界限,前半部分主要是计算扩容后新哈希桶的长度,后半部分主要是对老哈希桶数据的迁移
二、计算扩容长度
这部分没有要太多说明的地位,主要是计算规则的采用的二倍扩容,之所以采取二倍,是因为哈希桶的长度要求是2的幂次方,如果是四倍或者更大的话,则容易造成空间的浪费,所以二倍扩容是比较合适的
三、数据迁移
1、移动的位置:
源码上是e.hash & (newCap - 1),而在put方法上是(n - 1) & hash,两者都是用hash值与现有哈希桶长度-1来进行与运算,假设初始长度是16,则n-1为15,二进制为11111,而扩容后长度为32,则newCap - 1为31,二进制为111111,后者的计算结果差值为100000,转为十进制则为16,也就是原哈希桶的长度,因为移动后的位置要么就是原索引位置,要么就是原索引位置+原哈希桶长度,这应该也算是hash寻址的特点之一吧
2、链表的转移
因为上述的移动位置的特性,JDK1.8对于链表做了算法优化,它先将链表拆分成lo链表和hi链表,两个链表,然后将两个链表挂别挂在上述指定的两个位置上,省去了对链表一个个寻址挂载的步骤,提高了效率,这里还需要注意是,两个链表的数量并不是平均的,也没有固定的顺序逻辑