第 4 篇:HashMap 深度解析(JDK1.7 vs JDK1.8、红黑树、扩容逻辑)(5 题)

面试题 1:JDK1.7 与 JDK1.8 的 HashMap 底层结构有什么区别?

HashMap 是 Java 面试中出现率最高的数据结构之一,这一题回答得好坏会直接体现你的深度。

许多人只会说:"1.8 加了红黑树",但真正的差异远不止这个。


一、JDK 1.7 HashMap:数组(table)+ 链表(Entry)

底层结构:

java 复制代码
Entry[] table;

特点:

  • 使用 头插法(链表插入时把新节点放到头部)
  • 扩容时容易出现链表反转
  • 多线程扩容存在"死链"风险(环形链表)

二、JDK 1.8 HashMap:数组 + 链表 + 红黑树(Node / TreeNode)

底层结构:

java 复制代码
Node[] table;

关键改进:

  1. 链表长度超过阈值(默认 8)→ 转换为红黑树 TreeNode
  2. 使用尾插法(避免死链问题)
  3. 扩容逻辑完全重写,更高效

三、为什么要引入红黑树?

链表在最坏情况下会退化为 O(n) 查找,而树化后:

复制代码
链表查找:O(n)
红黑树查找:O(log n)

极大提升性能,尤其应对 高 hash 冲突 / 恶意攻击


面试官追问

  1. 为什么链表长度阈值是 8?不是 6 或 10?
  2. 为什么 table 容量小于 64 时不树化?
  3. HashMap 中"树化"与"退化"为链表的条件是什么?

易错点

  • 错误认为"树化是为了更快",但更重要的是防御攻击
  • 不清楚 1.7 会出现链表反转和死链
  • 忽略尾插法对多线程场景的安全意义


面试题 2:HashMap 为什么要使用扰动函数(hash 方法)?底层如何计算桶位置?

初学者往往以为 HashMap 的下标由 hashCode() 直接决定,但实际并没有这么简单。


一、扰动函数的作用

JDK 8 的 HashMap 中,hash 计算如下:

java 复制代码
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

目的是:

  1. 高位参与运算,降低 hash 冲突概率
  2. 减少某些 hashCode() 设计不佳的类带来的退化链表
  3. 提升桶分布的均匀性

二、桶下标计算公式

java 复制代码
index = (n - 1) & hash

其中:

  • n 是 table 的长度(必须为 2 的幂)
  • 位与运算比取模运算更快

三、为什么 HashMap 必须使用 2 的幂作为数组长度?

因为通过位运算:

  • (n - 1) & hash 能得到更好的分布
  • 扩容为 2 倍后,新元素的位置只在两个桶之间切换
  • 扩容时无需重新 hash(性能更高)

面试官追问

  1. 为什么不采用素数作为数组长度?
  2. 扰动函数为什么要右移 16 位?
  3. 1.7 和 1.8 的 hash 算法区别是什么?

易错点

  • 不知道"位与"运算替代了取模
  • 不理解 HashMap 长度必须为 2 的幂
  • 认为 hash 方法只是为了"唯一性"(错)


面试题 3:HashMap 的扩容机制是什么?为什么扩容时不需要重新计算 hash?

扩容是 HashMap 最核心的逻辑之一,必须讲清楚位运算特性。


一、扩容触发条件

java 复制代码
size >= threshold  // threshold = capacity × loadFactor

默认:

复制代码
loadFactor = 0.75
初始容量 16 → 阈值 12

当 size > 12 时触发扩容。


二、扩容后的容量

java 复制代码
newCapacity = oldCapacity * 2

HashMap 始终保证容量为 2 的幂。


三、扩容不需要重新计算 hash,为什么?

因为:

java 复制代码
(h & oldCap) == 0 → 元素留在原位置
(h & oldCap) != 0 → 元素移动到原位置 + oldCap

举例说明:

旧容量 = 16(10000)

扩容后 = 32(100000)

旧索引 = hash & 15

新索引可能为:

  • oldIndex
  • oldIndex + 16

只需判断 hash 的某一位是否为 1。

这就是 位运算加速扩容的精髓


四、扩容效率为何如此高?

因为避免了:

  • 重新计算 hash
  • 逐个节点重新定位
  • 整体 rehash

面试官追问

  1. HashMap 为什么默认负载因子是 0.75?
  2. 为什么扩容是 2 倍,而不是 1.5 倍或 3 倍?
  3. 扩容时链表是否保持节点原顺序?

易错点

  • 不知道扩容节点只会去"两个位置"
  • 不知道 resize 会重建链表/树结构
  • 认为扩容一定会重算 hash


面试题 4:HashMap 中链表转换为红黑树(树化)的条件是什么?什么时候会退化回链表?

这是 1.8 最重要的改动之一。


一、树化条件(必须同时满足)

✔ 1. 链表长度 >= 8
java 复制代码
TREEIFY_THRESHOLD = 8
✔ 2. table 容量 >= 64
java 复制代码
MIN_TREEIFY_CAPACITY = 64

如果容量太小,树化反而浪费性能,因此优先扩容。


二、为什么链表长度阈值是 8?

因为根据 泊松分布

  • 链表长度超过 8 的概率极低
  • 超过这个长度再树化,性价比最好
  • 不会导致大规模红黑树创建

三、红黑树退化为链表的条件

java 复制代码
UNTREEIFY_THRESHOLD = 6

即链表长度减少到 6 以下时,自动退化为链表。


面试官追问

  1. 为什么树化阈值是 8,而退化阈值是 6?
  2. 红黑树在 HashMap 中的节点结构是什么样的?
  3. 树化操作是否会影响其他桶?

易错点

  • 以为链表长度 > 8 就必定树化(忽略容量限制)
  • 只背数字,不理解设计原理
  • 认为树化是为了让 HashMap 更"高级"(误)


面试题 5:为什么 HashMap 在多线程环境下不安全?JDK1.7 为什么会出现死循环?

这是 HashMap 面试中最容易区分快速学习者与真正理解者的题。


一、HashMap 的线程不安全表现在:

  1. put 覆盖已有数据
  2. 扩容过程出现可见性问题(数据不一致)
  3. 遍历过程中结构修改导致 fail-fast
  4. JDK 1.7 扩容可能出现环形链表(死循环)

二、JDK 1.7 死循环问题(经典考点)

由于链表采用 头插法 ,扩容迁移过程中会出现 链表反转

多线程情况:

A 扩容迁移中,B 也迁移

→ 可能导致环形链表

→ 调用 get 时进入无限循环

这是面试官非常喜欢问的点。


三、JDK 1.8 为什么不再死循环?

因为:

  • 使用 尾插法(保持链表顺序)
  • 扩容逻辑重新设计,避免链表反转
  • 整体链表迁移可控

四、要线程安全的映射结构应该选什么?

ConcurrentHashMap

✔ 或者使用 Collections.synchronizedMap(new HashMap())


面试官追问

  1. JDK 1.7 的死循环触发条件是什么?
  2. 为什么 ConcurrentHashMap 不会出现类似问题?
  3. HashMap 在并发读写下会出现什么异常?

易错点

  • 以为 HashMap 经常死循环(实际上极难遇到,需要多线程 + 扩容触发)
  • 认为 1.8 完全线程安全(错误)
  • 忘记线程安全映射应该用 ConcurrentHashMap

本篇总结

本篇从底层原理到源码逻辑详尽总结了:

  • JDK1.7 vs 1.8 HashMap 的关键差异(链表 → 红黑树)
  • 扰动函数用法与为什么 table 必须为 2 的幂
  • HashMap 扩容机制:位运算判断新位置,无需重新计算 hash
  • 树化与退化的条件与原理
  • 多线程下 HashMap 的风险与 1.7 死循环的原因

HashMap 是 Java 面试的"重中之重",理解这一篇内容几乎可以覆盖 90% 的集合框架面试题。

相关推荐
听风吟丶2 小时前
微服务性能压测与容量规划实战:从高并发稳定性到精准资源配置
java·开发语言
在黎明的反思2 小时前
c++20协程
java·前端·c++20
用户12039112947262 小时前
从原生 JS 到 Vue3 Composition API:手把手教你用现代 Vue 写一个优雅的 Todos 任务清单
前端·vue.js·面试
gis分享者2 小时前
2023A卷,完美走位
面试·2023·真题·a·完美·走位·解答
小满、2 小时前
Redis:数据结构与基础操作(String、List、Hash、Set、Sorted Set)
java·数据结构·redis·分布式锁
alien爱吃蛋挞2 小时前
【JavaEE】Spring Boot日志
java·数据库·spring boot
浮游本尊3 小时前
Java学习第31天 - 高级主题与深度实战
java
BD_Marathon3 小时前
【JavaWeb】IDEA关联Tomcat并使用Tomcat运行JavaWeb项目
java·tomcat·intellij-idea
柒.梧.3 小时前
手写Tomcat的实现代码分享
java·tomcat