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 负载因子不变。

这种设计保证:

  • 底层数组长度始终保持为 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;
                }
            }
        }
    }
}

分析节点重分布过程

  1. 遍历旧数组

    • 对于每个桶(下标 j),如果该桶非空,则将其链表或树状结构中的节点迁移到新数组中。
  2. 单个节点的情况

    • 如果桶中只有一个节点(e.next == null),直接用新哈希值计算新位置:
      newTab[e.hash & (newCap - 1)] = e;
      
    • 这步利用了 newCap 为2的幂 的特点,直接使用位运算获得新索引。
  3. TreeNode 的情况

    • 如果节点属于红黑树结构,则调用 split() 方法让树节点自行完成分割和再组织。
  4. 链表节点(多个元素)的情况

    • 为了避免在扩容过程中破坏原有节点链表的顺序,该过程将链表分成两部分:
      • loList:所有 (e.hash & oldCap) == 0 的节点,这部分节点在扩容后索引保持不变。
      • hiList:所有 (e.hash & oldCap) != 0 的节点,它们在扩容后索引会加上 oldCap,即迁移到 j + oldCap 位置。
    • 这种分割方法利用了扩容时多出的那一位(oldCap 对应的位):
      • 若该位为 0,则新索引与原索引相同;
      • 若为 1,则新索引为原索引加上 oldCap。
  5. 重新连接链表

    • 对于分成 lo 和 hi 两部分的节点,分别断开尾部的 next 链接,并放入新数组相应位置。

这种分离和重新连接策略可以在一次遍历中完成节点重新分布,并且仅通过简单的位运算判断而无需重新计算哈希函数,有效地保证了扩容效率。


7. 返回扩容后的新数组

return newTab;
  • 说明
    • 扩容完成后返回新的数组,此时所有节点已经根据新容量(newCap)重新分配到了各自的桶中。

总结

  • 扩容两倍的优势

    • 通过保持 容量为 2 的幂次,新旧扩容后的索引计算只需做一次位运算判断((e.hash & oldCap) == 0)即可确定节点的新位置。
    • 这种方式简化了 rehash 的过程,保证了扩容时的节点重新分布既高效又能保持原有顺序(对于链表节点而言)。
  • 阈值同步翻倍

    • 同时阈值也翻倍,确保负载因子(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;
}
posted @ 2025-04-22 14:39  kuki'  阅读(13)  评论(0)    收藏  举报