HashMap的扩容和迁移

一、HashMap 扩容核心流程

HashMap 底层是「数组 + 链表 / 红黑树」结构,当元素数量达到阈值时,会触发扩容 ------ 本质是创建更大的数组,并将旧数组中的所有元素迁移到新数组,以保证查询效率。

1. 触发条件

  • 通用条件(1.7/1.8 共用)size > Hash表容量 * 0.75(负载因子默认 0.75)
  • 1.8 新增条件:链表树化判断 → 当某个链表长度 > 8,但数组长度 < 64 时,也会触发扩容(优先扩容而非树化)

2. 创建新数组

扩容时会创建一个容量为原数组 2 倍的新数组(保证容量始终是 2 的幂,便于位运算优化):

复制代码
int oldCap = oldTab.length;
int newCap = oldCap << 1; // 等价于 oldCap * 2
Node<K,V>[] newTab = (Node<K,V>[]) new Node[newCap];

3. 元素迁移:核心四部曲

迁移前先明确关键规则:

  • 旧下标:index = hash & (oldCap - 1)
  • 新容量:newCap = 2 * oldCap
  • 新下标判断:
    • hash & oldCap == 0 → 新下标 = 旧下标
    • hash & oldCap == 1 → 新下标 = 旧下标 + 旧容量

迁移时按以下四步处理旧数组的每个桶:

第一步:空桶直接跳过

遍历旧数组时,若当前位置为 null,则无需处理,直接跳过。

第二步:单个节点直接计算新下标

若桶中只有一个节点,直接计算新下标并放入新数组:

复制代码
newTab[e.hash & (newCap - 1)] = e;

第三步:链表节点拆分迁移

若桶中是链表,按 hash & oldCap 拆分为低位链表(low)高位链表(high)

  • 低位链表:新下标 = 旧下标
  • 高位链表:新下标 = 旧下标 + 旧容量
  • 采用尾插法保持原链表顺序,避免并发死循环

第四步:红黑树节点拆分迁移

若桶中是红黑树,同样按 hash & oldCap 拆分为两棵树:

  • 统计每棵树的节点数:
    • 节点数 ≤ 6 → 降级为链表(避免小数据量下红黑树的性能开销)
    • 节点数 > 6 → 保持红黑树结构,重新调整平衡
  • 低位树 → 新下标 = 旧下标
  • 高位树 → 新下标 = 旧下标 + 旧容量

二、JDK 1.7 与 1.8 扩容迁移差异对比

表格

维度 JDK 1.7 JDK 1.8
插入方式 头插法(扩容后链表会倒置) 尾插法(保持原链表顺序)
迁移方式 重新计算每个节点的下标,逐个插入新容器,效率低 拆分链表 / 红黑树,整体迁移,效率更高
并发安全性 头插法可能导致环形链表,引发死循环 尾插法避免链表反转,无环形链表问题,更安全
红黑树支持 无红黑树,冲突严重时链表过长,查询效率退化 链表长度 ≥ 8 时转为红黑树;扩容时节点数 ≤ 6 则退化为链表

三、关键原理:为什么 hash & oldCap 能判断新下标?

HashMap 数组容量始终是 2 的幂(如 16 → 32):

  • 旧容量 oldCap = 16 → 二进制 10000
  • 新容量 newCap = 32 → 二进制 100000

下标计算本质是 hash % 容量,优化为 hash & (容量 - 1)

  • 旧下标:hash & 15(二进制 01111
  • 新下标:hash & 31(二进制 11111

新下标仅比旧下标多判断最高一位二进制位 ,而 hash & oldCap 正好能判断这一位:

  • 结果为 0 → 该位是 0 → 新下标 = 旧下标
  • 结果为 1 → 该位是 1 → 新下标 = 旧下标 + 旧容量

示例

  • 元素 A:hash = 5(二进制 00101)→ 5 & 16 = 0 → 新下标 = 原下标 5
  • 元素 B:hash = 21(二进制 10101)→ 21 & 16 = 16 → 新下标 = 5 + 16 = 21

四、面试高频考点总结

  1. 扩容触发条件size > 容量 * 0.75(1.7/1.8 共用);1.8 新增「链表长度 > 8 且数组长度 < 64」时优先扩容。
  2. 新下标计算 :利用 hash & oldCap 快速判断,避免重复计算 hash % 新容量
  3. 1.7 与 1.8 核心差异:头插法 vs 尾插法、逐个迁移 vs 整体拆分、无红黑树 vs 红黑树支持。
  4. 并发安全问题:1.7 头插法可能导致环形链表死循环,1.8 尾插法解决此问题。
  5. 红黑树降级规则:扩容后树节点数 ≤ 6 时退化为链表,避免频繁树化 / 链表化切换。
相关推荐
一叶落4382 小时前
LeetCode 380. O(1) 时间插入、删除和获取随机元素【哈希表 + 动态数组 | C语言详解】
c语言·数据结构·c++·算法·leetcode·散列表
xiaoye-duck2 小时前
《算法题讲解指南:递归,搜索与回溯算法--二叉树中的深搜》--8.二叉树剪枝,9.验证二叉搜索树
c++·算法·深度优先·递归
像素猎人3 小时前
数据结构之——图论中常用的方向数组是如何定义的
算法
卷福同学11 小时前
QClaw内测体验,能用微信指挥AI干活了
人工智能·算法·ai编程
sali-tec11 小时前
C# 基于OpenCv的视觉工作流-章34-投影向量
图像处理·人工智能·opencv·算法·计算机视觉
xiaoye-duck11 小时前
《算法题讲解指南:递归,搜索与回溯算法--递归》--3.反转链表,4.两两交换链表中的节点,5.快速幂
数据结构·c++·算法·递归
Eward-an11 小时前
【算法竞赛/大厂面试】盛最多水容器的最大面积解析
python·算法·leetcode·面试·职场和发展
山栀shanzhi11 小时前
归并排序(Merge Sort)原理与实现
数据结构·c++·算法·排序算法
阿豪学编程11 小时前
LeetCode438: 字符串中所有字母异位词
算法·leetcode