Java HashMap 的扩容机制是一个核心功能,用以保证 HashMap 性能随着元素的增加而适度下降。这里将详细介绍其扩容机制。
扩容机制的工作流程:
-
触发条件:
当 HashMap 中的元素数量超过threshold
(阈值)时,触发扩容。这个阈值是当前容量(capacity
)与负载因子(loadFactor
,默认值为0.75)的乘积。 -
新容量计算:
新的容量是当前容量的两倍,HashMap 容量总是保持 2 的幂次。 -
创建新数组:
新的 Node 数组(桶)被创建,大小为新的容量。 -
重新哈希:
现有的键值对必须被移到新的桶中。这需要对每个键重新计算其在新数组中的位置。 -
重新分配元素:
对于旧数组中的每个桶,遍历桶中的每个节点(可能是链表或红黑树),并将它们移动到新的桶中。在这个过程中,由于数组大小翻倍,某些元素的位置会发生变化。
resize
方法源码解析:
// HashMap的resize方法源码片段(简化版,基于Java 8)
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 (loHead != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiHead != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
在 resize
方法中,首先计算新的容量和阈值。然后,创建一个新的 Node 数组作为新的桶。对于旧数组中的每个非空桶,将遍历其中的节点,并根据其哈希值在新的桶中重新分配位置。如果节点是一个红黑树节点,会调用 split
方法来处理。如果节点是链表,会维护两个链表头,一个链表头用来处理位置不变的元素,另一个链表头用来处理位置发生变化的元素(即原索引位置加上旧容量的位置)。
代码演示:
// 创建一个小容量的HashMap来演示扩容
HashMap<Integer, String> map = new HashMap<>(2);
map.put(1, "a"); // 添加元素,触发扩容
map.put(2, "b"); // 继续添加元素
map.put(3, "c"); // 再次触发扩容
// 打印出HashMap的状态来观察元素的分布
for (Map.Entry<Integer, String> entry : map.entrySet()) {
System.out.println("Key: " + entry.getKey() + " - Value: " + entry.getValue());
}
// 打印出实际的桶数量(capacity)
System.out.println("HashMap capacity: " + getField(map, "table").length);
这段代码演示了一个 HashMap
的创建和扩容过程。创建时指定了初始容量为 2,随着元素的添加,它会自动扩容以保持预定的负载因子。
请注意,getField(map, "table").length
这个调用用来检查内部数组的大小,需要反射来实现,这里只是为了演示,实际中并不推荐这样做,因为内部实现细节可能会变化,并且反射会绕过访问控制而破坏封装性。
HashMap 的扩容是一个成本较高的操作,因为它涉及到重新计算元素的位置并重新分配它们。这也是为什么在创建 HashMap
时提供初始容量和负载因子是一个好的实践,尤其是当你预期会有大量元素存储在 HashMap
中时。这样可以减少扩容的次数,从而提高整体性能。