HashMap JDK1.8源码
分析
左移1,导致扩容2倍
源码解析
1. 初始变量设置与容量计算
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
说明:
- 获取当前使用的数组 oldTab 以及其容量 oldCap。
- threshold 表示扩容阈值(通常是 capacity 与 loadFactor 的乘积)。
- 如果 oldTab 为 null,说明当前 HashMap 还未初始化,后续会用默认的初始容量进行分配。
2. 当已存在数组时(即 oldCap > 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
}
-
容量限制判断:
- 如果当前容量已经达到允许的最大值,则将 threshold 设置为 Integer.MAX_VALUE,并直接返回,不再扩容。
-
扩容为两倍:
- 当条件满足时,newCap 计算为 oldCap 左移 1,即扩容为原来的两倍(
oldCap << 1
)。 - 同时,阈值 newThr 也翻倍(
oldThr << 1
),保证 loadFactor 负载因子不变。
- 当条件满足时,newCap 计算为 oldCap 左移 1,即扩容为原来的两倍(
这种设计保证:
- 底层数组长度始终保持为 2 的幂次。这种幂次结构使得在计算新数组的索引时(使用
e.hash & (newCap - 1)
)可以借助位运算实现高效计算。 - 扩容后,只需要依靠原始哈希值中的一位(oldCap对应的位)就可以判断某个节点该“留在”原位还是迁移到新位置(即原索引加 oldCap 的位置)。
3. 初始情况(未初始化或设置了初始阈值)
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);
}
- 说明:
- 如果 oldCap 为 0 但阈值 oldThr 大于0,说明在构造中指定了初始容量;直接以 oldThr 作为 newCap。
- 如果两者都没有设置,则使用默认的初始容量和默认负载因子。
4. 如果 newThr 仍为 0,则根据公式计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
- 说明:
- 通过 newCap 与 loadFactor 的乘积计算出新的扩容阈值。如果计算结果在允许范围内,则用这个值,否则设置为 Integer.MAX_VALUE,确保后续不会因负载过高而发生无限扩容。
5. 更新 threshold、创建新数组并赋值给 table
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
- 说明:
- 更新 HashMap 的扩容阈值。
- 创建一个新的数组 newTab,用来存放扩容后的所有节点,并将 table 指向新数组。
6. 将旧数组中的节点重新分布到新数组(Rehash)
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;
}
}
}
}
}
分析节点重分布过程
-
遍历旧数组:
- 对于每个桶(下标 j),如果该桶非空,则将其链表或树状结构中的节点迁移到新数组中。
-
单个节点的情况:
- 如果桶中只有一个节点(
e.next == null
),直接用新哈希值计算新位置:newTab[e.hash & (newCap - 1)] = e;
- 这步利用了 newCap 为2的幂 的特点,直接使用位运算获得新索引。
- 如果桶中只有一个节点(
-
TreeNode 的情况:
- 如果节点属于红黑树结构,则调用
split()
方法让树节点自行完成分割和再组织。
- 如果节点属于红黑树结构,则调用
-
链表节点(多个元素)的情况:
- 为了避免在扩容过程中破坏原有节点链表的顺序,该过程将链表分成两部分:
- loList:所有
(e.hash & oldCap) == 0
的节点,这部分节点在扩容后索引保持不变。 - hiList:所有
(e.hash & oldCap) != 0
的节点,它们在扩容后索引会加上 oldCap,即迁移到j + oldCap
位置。
- loList:所有
- 这种分割方法利用了扩容时多出的那一位(oldCap 对应的位):
- 若该位为 0,则新索引与原索引相同;
- 若为 1,则新索引为原索引加上 oldCap。
- 为了避免在扩容过程中破坏原有节点链表的顺序,该过程将链表分成两部分:
-
重新连接链表:
- 对于分成 lo 和 hi 两部分的节点,分别断开尾部的 next 链接,并放入新数组相应位置。
这种分离和重新连接策略可以在一次遍历中完成节点重新分布,并且仅通过简单的位运算判断而无需重新计算哈希函数,有效地保证了扩容效率。
7. 返回扩容后的新数组
return newTab;
- 说明:
- 扩容完成后返回新的数组,此时所有节点已经根据新容量(newCap)重新分配到了各自的桶中。
总结
-
扩容两倍的优势:
- 通过保持 容量为 2 的幂次,新旧扩容后的索引计算只需做一次位运算判断(
(e.hash & oldCap) == 0
)即可确定节点的新位置。 - 这种方式简化了 rehash 的过程,保证了扩容时的节点重新分布既高效又能保持原有顺序(对于链表节点而言)。
- 通过保持 容量为 2 的幂次,新旧扩容后的索引计算只需做一次位运算判断(
-
阈值同步翻倍:
- 同时阈值也翻倍,确保负载因子(loadFactor)保持不变,预防在同样的负载下频繁扩容。
-
不同结构的节点处理:
- 对于单节点和树状结构节点,采取直接移动或特定分割策略,确保不同情况下都能高效扩容。
这段源码展示了 HashMap 为何采用扩容两倍的策略——既利用了数学上的位运算优势,也确保了扩容过程中节点再分布的最小开销,同时保持数据结构内在的高效性和稳定性。
通过以上分析,你应该能够理解 Java 8 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;
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;
}