HashMap 扩容全流程

一、扩容基础概念

1. 什么是扩容?

HashMap 底层是数组 + 链表 / 红黑树 ,数组是定长结构。当元素越来越多,哈希冲突变多、性能下降,因此需要:

  • 创建新的更大数组
  • 重新计算所有元素位置
  • 把旧数据转移到新数组这个过程就是扩容(resize)

2. 扩容核心目标

  1. 减少哈希冲突,提升查询 / 插入效率
  2. 保证数组空间利用率与性能平衡
  3. 始终维持容量为 2 的幂次方

二、扩容通用核心参数

所有版本都依赖这 3 个参数决定扩容:

  1. 容量(capacity)
    • 数组长度,默认 16,必须是 2^n
  2. 负载因子(loadFactor)
    • 默认 0.75,时间 / 空间平衡最佳值
  3. 阈值(threshold)
    • 阈值 = 容量 × 负载因子
    • 元素数量 超过阈值 就触发扩容

三、扩容触发条件(所有版本一致)

满足任意一个就触发:

  1. 新增元素后,size > threshold(最常见)
  2. 链表要树化,但数组长度 < 64(JDK 1.8)
  3. 初始化 HashMap 时,第一次 put 元素(懒加载)

四、JDK 1.8 扩容全流程(重点)

1. 扩容整体流程

  1. 旧数组为空 → 初始化默认容量(16)、阈值(12)
  2. 旧数组非空 → 新容量 = 旧容量 ×2,新阈值也 ×2
  3. 创建新的空数组
  4. 遍历旧数组每个位置
  5. 对每个位置的节点分三种情况处理:
    • 无节点:跳过
    • 单节点:重新计算下标放入新数组
    • 链表节点:高低位拆分,放入原位置 / 原位置 + 旧容量
    • 红黑树节点:树拆分 + 退化判断
  6. 新数组替换旧数组,扩容完成

2. JDK 1.8 扩容源码关键逻辑

java

运行

复制代码
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;

    // 1. 计算新容量、新阈值
    if (oldCap > 0) {
        // 容量翻倍
        newCap = oldCap << 1; 
        newThr = oldThr << 1; 
    }
    // 2. 初始化逻辑(第一次put)
    else if (oldThr > 0) {
        newCap = oldThr;
    } else {
        newCap = DEFAULT_INITIAL_CAPACITY; // 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12
    }

    threshold = newThr;
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;

    // 3. 旧数据转移到新数组(核心)
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;

                // 情况1:单个节点
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;

                // 情况2:红黑树节点
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                // 情况3:链表节点(高低位拆分)
                else {
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 高位=0 → 留在原下标
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 高位=1 → 新下标=原下标+旧容量
                        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;
}

3. JDK 1.8 扩容核心创新:高低位拆分

原理

容量是 2 的幂,扩容后:

  • 新增最高一位二进制
  • hash & oldCap 判断这一位是 0 还是 1

结果只有两种:

  1. 0 → 留在原下标
  2. 1 → 新下标 = 原下标 + 旧容量

优势

  • 不需要重新计算 hash
  • 不需要取模运算
  • 链表保持原有顺序,避免死循环

五、JDK 1.7 扩容全流程(经典对比)

1. 核心流程

  1. 新容量 = 旧容量 ×2
  2. 新阈值 = 新容量 × 负载因子
  3. 创建新数组
  4. 遍历旧数组 → 遍历每个链表
  5. 头插法重新插入到新数组
  6. 替换数组,完成扩容

2. JDK 1.7 关键源码

java

运行

复制代码
void resize(int newCapacity) {
    Entry[] oldTable = table;
    int oldCapacity = oldTable.length;
    // 1. 创建新数组
    Entry[] newTable = new Entry[newCapacity];
    // 2. 旧数据转移(核心)
    transfer(newTable, initHashSeedAsNeeded(newCapacity));
    table = newTable;
    threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
}

void transfer(Entry[] newTable, boolean rehash) {
    int newCapacity = newTable.length;
    for (Entry<K,V> e : oldTable) {
        while(null != e) {
            Entry<K,V> next = e.next;
            if (rehash) {
                e.hash = null == e.key ? 0 : hash(e.key);
            }
            // 重新计算下标
            int i = indexFor(e.hash, newCapacity);
            // 头插法!!!
            e.next = newTable[i];
            newTable[i] = e;
            e = next;
        }
    }
}

3. JDK 1.7 致命问题

多线程并发扩容 → 链表环 + 死循环

原因:

  • 采用头插法
  • 链表顺序会被反转
  • 多线程下指针互相指向,形成闭环
  • 一旦 get 数据,就会无限循环 CPU 100%

六、JDK 1.7 VS JDK 1.8 扩容逐点拆解(面试必背)

表格

维度 JDK 1.7 JDK 1.8
数据结构 数组 + 链表 数组 + 链表 + 红黑树
插入方式 头插法 尾插法
链表顺序 扩容后反转 保持原顺序
下标计算 全部重新 hash % 容量 高低位拆分,无需重算
并发风险 链表环、死循环 无线程安全问题,但仍非线程安全
转移效率 低(频繁计算 + 冲突) 高(优化拆分)
树化逻辑 支持树拆分、退化
空节点处理 直接遍历 提前置空,帮助 GC

七、扩容高频面试题(标准答案)

1. 为什么 HashMap 扩容是 2 倍?

  1. 保证容量始终是2 的幂
  2. 下标计算用 hash & (len-1),比取模更快
  3. 扩容可使用高低位拆分,效率极高
  4. 数据分布均匀,减少哈希冲突

2. JDK 1.7 为什么会出现死循环?

  • 头插法
  • 多线程并发扩容
  • 链表反转,形成环形链表
  • 查询时进入死循环,CPU 飙升

3. JDK 1.8 解决死循环了吗?

  • 解决了!
  • 采用尾插法,保持链表顺序
  • 不会形成闭环

4. 1.8 扩容高低位拆分原理?

  • 扩容后新增一个高位 bit
  • hash & oldCap 判断该位
  • 0 → 原位置
  • 1 → 原位置 + 旧容量
  • 无需重算 hash,效率极高

5. 扩容为什么非常消耗性能?

  • 需要新建数组
  • 需要重新排布所有数据
  • 时间复杂度 O(n)
  • 因此建议:初始化时指定容量,减少扩容次数

八、最佳实践

  1. 初始化指定容量

    java

    运行

    复制代码
    Map<String, Object> map = new HashMap<>(16);
  2. 高并发场景绝对不用 HashMap,用 ConcurrentHashMap

  3. 尽量使用不可变对象作 key(String、Integer)

  4. 避免频繁扩容,提升性能


九、总结(极简背诵版)

  1. 扩容触发:size > 阈值 或 树化前数组不足 64
  2. 扩容大小 :始终 2 倍扩容
  3. JDK1.7 :头插法、链表反转、多线程死循环
  4. JDK1.8 :尾插法、高低位拆分、无死循环、效率更高
  5. 核心优化:1.8 扩容不再重新计算哈希,直接按高低位拆分
  6. 容量必须是 2 的幂:为了位运算高效 + 数据均匀
相关推荐
历程里程碑1 小时前
链表--LRU缓存
大数据·数据结构·elasticsearch·链表·搜索引擎·缓存
阿崽meitoufa1 小时前
抽象类 接口 内部类
java·开发语言
代码探秘者1 小时前
【算法篇】4.前缀和
java·数据库·后端·python·算法·spring
计算机安禾2 小时前
【数据结构与算法】第4篇:算法效率衡量:时间复杂度和空间复杂度
java·c语言·开发语言·数据结构·c++·算法·visual studio
蓝色心灵-海2 小时前
小律书 技术架构详解:前后端分离的自律管理系统设计
java·http·小程序·架构·uni-app
华科易迅2 小时前
Spring AOP(XML最终+环绕通知)
xml·java·spring
IT观测2 小时前
深度分析俩款主流移动统计工具Appvue和openinstall
android·java·数据库
Oueii2 小时前
嵌入式LinuxC++开发
开发语言·c++·算法
华科易迅2 小时前
Spring AOP(注解前置+后置通知)
java·后端·spring