Java基础-HashMap扩容机制(Java8源码)

一、初始化阶段(确定新容量和阈值)

java 复制代码
final Node<K,V>[] resize() {
    // 1. 保存旧的哈希表
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;  // 旧容量
    int oldThr = threshold;  // 旧阈值
    int newCap, newThr = 0;  // 新容量,新阈值(初始为0)

二、计算新容量和新阈值(三种情况)

情况1:正常扩容(已有数据)

java 复制代码
if (oldCap > 0) {  // 哈希表已初始化
    if (oldCap >= MAXIMUM_CAPACITY) {  // 已达最大容量(2^30)
        threshold = Integer.MAX_VALUE;  // 阈值设为最大整数
        return oldTab;  // 不再扩容,直接返回
    }
    // 容量翻倍(左移1位),且未超过最大容量
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
             oldCap >= DEFAULT_INITIAL_CAPACITY)
        newThr = oldThr << 1; // 阈值也翻倍
}

情况2:首次初始化(通过构造器指定了容量)

java 复制代码
else if (oldThr > 0)  // 阈值已设置(构造器指定了初始容量)
    newCap = oldThr;  // 新容量 = 旧阈值

情况3:首次初始化(使用默认值)

java 复制代码
else {  // 完全默认初始化
    newCap = DEFAULT_INITIAL_CAPACITY;  // 默认容量16
    newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  // 默认阈值12
}

补充计算新阈值

java 复制代码
if (newThr == 0) {  // 前两种情况可能需要重新计算阈值
    float ft = (float)newCap * loadFactor;  // 容量 × 负载因子
    newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
              (int)ft : Integer.MAX_VALUE);
}
threshold = newThr;  // 更新阈值

三、创建新哈希表并迁移数据

java 复制代码
// 1. 创建新的数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;

// 2. 迁移数据(如果旧表不为空)
if (oldTab != null) {
    for (int j = 0; j < oldCap; ++j) {  // 遍历每个桶
        Node<K,V> e;
        if ((e = oldTab[j]) != null) {  // 桶不为空
            oldTab[j] = null;  // 清空旧桶,帮助GC
            
            // 情况A:单个节点
            if (e.next == null)
                newTab[e.hash & (newCap - 1)] = e;  // 直接放入新桶
            
            // 情况B:红黑树节点
            else if (e instanceof TreeNode)
                ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
            
            // 情况C:链表节点(重点!保持顺序的优化)
            else {
                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 {
                        // 去高位(索引 = 原索引 + oldCap)
                        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;  // 原位置+oldCap
                }
            }
        }
    }
}
return newTab;

四、核心扩容机制解析

1. 扩容时机

  • 当元素数量 > 容量 × 负载因子(默认0.75)时触发
  • 例如:默认容量16,阈值12,插入第13个元素时扩容

2. 扩容大小

  • 新容量 = 旧容量 × 2(除非已达最大容量)
  • 新阈值 = 旧阈值 × 2

3. 数据迁移优化(核心亮点)

传统方式 :对每个节点重新计算 hash & (newCap-1)

Java 8优化方式 :利用位运算 (e.hash & oldCap)

  • 因为容量总是2的幂,所以扩容相当于在二进制高位加1
  • (e.hash & oldCap) == 0:节点留在原位置(低位链表)
  • (e.hash & oldCap) != 0:节点移到原索引+oldCap位置(高位链表)

示例

  • 旧容量:16(10000二进制)
  • 新容量:32(100000二进制)
  • 假设hash=01010,原索引=01010 & 01111 = 01010 = 10
  • 判断:(01010 & 10000) = 0 → 留在位置10
  • 假设hash=11010,原索引=11010 & 01111 = 01010 = 10
  • 判断:(11010 & 10000) = 10000 ≠ 0 → 移到位置10+16=26

4. 保持顺序

  • Java 8在链表迁移时保持了原有顺序
  • 避免了Java 7中可能出现的死循环问题
  • 同时提高了局部性,对缓存更友好

5. 多线程问题

  • HashMap本身不是线程安全的
  • 多线程环境下扩容可能导致数据丢失、链表成环等问题
  • 建议使用ConcurrentHashMap或Collections.synchronizedMap

五、总结特点

  1. 倍增扩容:容量总是2的幂,扩容时翻倍
  2. 高效迁移:利用位运算代替模运算,避免重新hash
  3. 保持顺序:链表节点保持原有顺序
  4. 分类处理:对单个节点、链表、红黑树分别处理
  5. 延迟初始化:首次put时才真正创建数组

这种设计在保证性能的同时,减少了哈希冲突,提高了查询效率。

相关推荐
li.wz2 小时前
ShardingSphere 与 PolarDB-X 选型对比
java·后端·微服务
wanghowie2 小时前
02.02.02 CompletableFuture 组合与异常处理:构建复杂异步流
java·future·并发编程
代码or搬砖2 小时前
Collections和Arrays
java·开发语言
Yiii_x2 小时前
Object类与包装类
java·经验分享·笔记·课程设计·ai编程
吴名氏.2 小时前
电子书《Java程序设计与应用开发(第3版)》
java·开发语言·java程序设计与应用开发
喵手2 小时前
数字处理的那些事:从 `Math` 到 `BigDecimal`,如何玩转数字与随机数?
java·数字处理
Wang15302 小时前
2025-2026 Java核心技术热点全景解析:从LTS革新到生态跃迁,筑牢后端技术核心竞争力
java
ss2732 小时前
ScheduledThreadPoolExecutor异常处理
java·开发语言
ssschema3 小时前
M4芯片MAC安装java环境
java·macos