← 每日面试题 学习看板

JDK1.8中HashMap有哪些改变?

面试题/00java/01Java 基础/05 集合/JDK1.8中HashMap有哪些改变?.md

面试记录 2026-10-09

题目:JDK1.8 中 HashMap 有哪些改变?

难度:medium

标签:java/collections, general/knowledge

评分:⭐⭐⭐☆☆(3/5)

我的回答

JDK 1.8 中 HashMap 的改动,首先核心是处理了并发下它的死循环问题,把原本的头插法改成了尾插法。头插法原本头插法出现,可能会出现,在断头的过程中,其他线程的插入,导致出现头指尾、尾指头,整个变成了死循环,产生导致程序崩溃的问题。然后其次在 JDK 1.8 中引入了,当单条链哈希冲突到 8 个以上的时候,会进行红黑树扩容。引入了红黑树这个架构数据结构,替换了原本的链表,提升了对于数据检索的效率。然后再然后是关于扩容方式,原本是通过扩容之后,每个元素进行哈希重新分配,修改后使用位运算,当新旧值对比位运算是的值是位运算是 0 的时候就不用移,如果是 1 的话就进行迁移到新的位置,扩容后的新的位置,只用移一部分元素,不用全部元素再进行迁移。

点评

优点:

不足:

补充(JDK8 HashMap 完整改动清单):

  1. 数据结构增强:数组 + 链表 + 红黑树三件套;引入 TreeNode(继承 LinkedHashMap.Entry)
  2. 头插改尾插:解决并发扩容环形链表问题(兼容性修复,HashMap 本身仍非线程安全)
  3. hash 算法升级:(h ^ (h >>> 16)) 高位参与运算
  4. 扩容位运算优化:e.hash & oldCap 判断原位或 +oldCap,避免重新 hash
  5. 转树双门槛:链长 > 8 且 数组容量 ≥ 64 才转树,防止小容量下误转
  6. 树退化:扩容后树桶 size < 6 退化回链表
  7. 新增函数式 API:compute、computeIfAbsent、computeIfPresent、merge

完整讲解

第一部分:知识重点剖析

核心考点定位:验证对 HashMap JDK7 → JDK8 演进的理解,尤其每个改动背后的性能与正确性权衡。

知识点拆解:

  1. 底层原理:JDK7 的 HashMap 是"数组 + 链表",性能瓶颈在长链表的 O(n) 查找和并发扩容的头插法循环。JDK8 引入红黑树(O(log n))+ 尾插法 + 位运算扩容优化,从"数据结构 → 冲突处理 → 扩容效率"三个维度同时优化。
  2. 改动 1:头插→尾插:JDK7 头插法在并发扩容时会反转链表,两个线程同时扩容时 A 遍历 (e→f),B 遍历 (f→e),A 完成后 e.next=f.next=e,形成 e→f→e 环,get 命中该桶 CPU 100%。JDK8 尾插法保持插入顺序,避免此问题——但 HashMap 本身仍非线程安全,只是这个特定 bug 修复了。
  3. 改动 2:hash 算法:hash(key) = (h = key.hashCode()) ^ (h >>> 16),把高 16 位异或到低 16 位。目的:让高位也参与桶下标计算,减少低位分布均匀时的高位碰撞。
  4. 改动 3:红黑树:链表长度 > 8 且容量 ≥ 64 时转红黑树;扩容时树桶 size < 6 退化回链表。8 来自泊松分布——自然概率链长到 8 的概率约 0.00000006,超过就是哈希冲突。
  5. 改动 4:扩容位运算:e.hash & oldCap 判断元素留原位还是搬到 index+oldCap。原理:容量是 2 的幂,扩容后新旧 index 只差第 n 位。避免重新 hash,节省约一半运算。
  6. 改动 5:TreeNode:JDK8 把 Node 拆分为 Node(链表)和 TreeNode(红黑树),TreeNode 继承 LinkedHashMap.Entry 保留双向链表语义,方便退化回链表时快速组装。
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. 为什么链表阈值选 8,而不是 4 或 16?
  1. 为什么 JDK8 引入红黑树后 HashMap 内存开销变大?
  1. computeIfAbsent 相比手写 if-null-then-put 有什么优势?
  1. 为什么即使 JDK8 修复了头插法循环,HashMap 仍然非线程安全?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,JDK8 对 HashMap 的改动主要集中在四个层面。

第一是数据结构,从"数组+链表"扩展为"数组+链表+红黑树"。当某个桶的链表长度超过 8 且整个 HashMap 的容量超过 64 时,链表会转换成红黑树,把该桶内的查找从 O(n) 降到 O(log n)。阈值 8 来自泊松分布,自然概率下链长到 8 几乎为零,超过说明是哈希冲突而非分布问题。扩容时如果树桶 size 小于 6 会退化回链表。

第二是 hash 算法的优化。JDK8 的 hash 函数是 hash(key) = key.hashCode() ^ (key.hashCode() >>> 16),把 hashCode 的高 16 位异或到低 16 位参与运算,让高位也参与桶下标的计算,减少碰撞。

第三是头插法改尾插法。JDK7 用头插法扩容时,两个线程并发操作会形成环形链表,get 命中该桶 CPU 会 100%。JDK8 用尾插法保留了插入顺序,避免这个问题。要注意这只是修复了特定的并发 bug,HashMap 本身仍然不是线程安全的,size 计数、并发 put 等都有问题,并发场景还是要用 ConcurrentHashMap。

第四是扩容的效率优化。JDK7 扩容时对每个元素重新计算 hash 定位新桶;JDK8 用 e.hash & oldCap 判断——如果结果非 0 说明元素 hash 的第 n 位是 1,新位置是 index + oldCap;如果为 0 说明第 n 位是 0,元素留在原位。原理是容量是 2 的幂,新旧 index 只在第 n 位有差别,一个 bit 判断就能决定迁移方向,省去了重新 hash 的开销。

此外还引入了 TreeNode 类(继承 LinkedHashMap.Entry,同时含树指针和双向链表指针),以及一批新的函数式 API:compute、computeIfAbsent、computeIfPresent、merge,配合 lambda 表达式的场景。总的来说 JDK8 从数据结构、hash 算法、并发正确性、扩容效率四个维度做了系统性优化,性能提升了大约 30%-50%。