面试必杀技:彻底搞懂 JDK 1.7 与 1.8 中 HashMap 扩容的四大核心区别
在 Java 面试中,如果你只知道 HashMap 怎么用,或者只能说出 JDK 1.8 里的单点知识,面试官往往会抛出一个经典连环炮:“那么请问,JDK 1.7 和 1.8 在 HashMap 的扩容实现上,具体有哪些区别?为什么要这么改?”
这个问题考察的不仅是你对 API 的使用,更是对底层数据结构演进及多线程并发问题的深刻理解。
今天,我们就把这四大核心区别扒得干干净净,并配上通俗易懂的解释,让你在面试时能够对答如流!
核心区别一:插入方式的改变(头插法 vs 尾插法)
在旧数组向新数组迁移数据,或者发生哈希冲突时向链表中插入新节点:
-
JDK 1.7:采用【头插法】
- 特点:新来的节点总是强行插在链表的最前面。
- 后果:每次扩容迁移后,原来在链表上的元素顺序会被彻底倒置(原本是 A -> B,迁移后变成了 B -> A)。设计者的初衷是认为“新插入的元素大概率会被最先访问”,但却埋下了大坑(见下文异常情况)。
-
JDK 1.8:改用【尾插法】
- 特点:新来的节点乖乖排在链表的最后面。
- 后果:扩容迁移后,链表元素的相对顺序保持不变,依然是 A -> B。这就从根本上规避了倒置带来的并发指针错乱问题。
核心区别二:迁移逻辑与效率(逐个重算 vs 高低位打包平移)
这是 JDK 1.8 对 HashMap 性能提升最惊艳的一笔!
-
JDK 1.7:低效的“重新计算”
- 扩容时,它需要把原数组中挂载的每一个节点单独摘下来,重新计算一次完整的 hash 下标(
hash & (newCap - 1)),然后再一个个put到新数组中。就像老房子搬家,要把所有东西一件件拆散,到了新家再一件件重新编排位置,费时费力。
- 扩容时,它需要把原数组中挂载的每一个节点单独摘下来,重新计算一次完整的 hash 下标(
-
JDK 1.8:高效的“整体打包拆分”
- 它完全抛弃了重新计算下标的做法!而是巧妙利用了
hash & 旧容量这一位运算,直接得出节点是留在原位(低位),还是加上旧容量后整体平移(高位)。 - 通俗理解:这就好比搬家时,直接把一整箱东西打包分为“留守派”和“搬迁派”两组,打包带走,极其高效。
- 它完全抛弃了重新计算下标的做法!而是巧妙利用了
核心区别三:并发安全性与夺命死循环(CPU 100% 惨案)
多线程环境下操作 HashMap 是非常危险的,而在扩容时更是灾难频发的高发区。
-
JDK 1.7:可怕的【环形链表】(死循环)
- 因为 1.7 使用的是头插法,在多线程并发扩容时,假如同一个槽位有多个节点需要迁移,线程 A 和线程 B 交替执行,极易导致原来节点之间互相引用的指针方向被反转(A 指向 B,B 又指回 A)。
- 后果:一旦后续有人调用
get()方法遍历这条链表,就会陷入死循环,直接导致服务器 CPU 飙升至 100%!
-
JDK 1.8:规避了死循环,但依然【非线程安全】
- 1.8 改成了尾插法,即使是多线程并发扩容,节点的相对顺序也不会反转,从根本上消除了环形链表死循环的隐患。
- 注意(面试必考踩坑点):虽然没有了死循环,但由于缺乏同步锁机制,在多线程 put 操作时,依然会发生数据互相覆盖导致数据丢失的问题。因此,多线程场景下请老老实实使用
ConcurrentHashMap!
核心区别四:底层数据结构的护城河(纯链表 vs 引入红黑树)
随着 HashMap 中数据量的剧增,极端情况下的哈希冲突会导致某一个数组槽位上的链表变得奇长无比。
-
JDK 1.7:硬着头皮遍历(纯数组 + 链表)
- 不管链表有多长,查找的时间复杂度都是 O(n)O(n)O(n)。如果一条链表挂了 100 个节点,你要找最后那个,就得遍历 100 次。
-
JDK 1.8:红黑树降维打击(数组 + 链表 + 红黑树)
- 升级树化:当单条链表长度
> 8并且总体数组长度> 64时,这条臃肿的链表会变身升级为红黑树,查找时间复杂度瞬间从 O(n)O(n)O(n) 降维到 O(logn)O(\log n)O(logn)。 - 扩容降级退化:在触发扩容的节点转移阶段,原本丰满的红黑树会被拆分成两棵树。如果拆分后某棵树的节点数量
<= 6,说明红黑树显得“杀鸡用牛刀”了,此时会自动退化(降级回链表)以节省内存和维护成本。
- 升级树化:当单条链表长度
面试高分背诵总结篇
当你被问及 1.7 与 1.8 扩容区别 时,请抛出以下黄金话术:
“面试官您好,JDK 1.7 和 1.8 在扩容及底层结构上有四大显著差异:
首先,插入顺序上,1.7 是头插法,1.8 改为了尾插法以保持迁移前后的节点顺序;
其次,并发隐患上,1.7 的头插法在多线程扩容时极易引发指针反转导致死循环(CPU 100%),而 1.8 的尾插法避免了死循环,但注意它依然不是线程安全的,会产生数据覆盖;
第三,迁移效率上,1.7 需要对每个节点重新计算哈希值再放入新数组,而 1.8 运用了神仙操作hash & oldCap的位运算,将长链表直接整体划分为高低两个部分,省去了重算过程,性能大幅提升;
最后,数据结构保障上,1.7 遇到极长链表查询会退化为 O(n)O(n)O(n),而 1.8 引入了红黑树,链表长度过长(超 8 且总容量超 64 )时会转树。并且在扩容切割红黑树节点期间,若切割后的树节点<= 6会自动退化回链表以节省开销。”
牢记这四点,横扫一切 HashMap 源码原理的拷问!
点赞收藏,随时复盘,大厂 Offer 拿到手软!
更多推荐





所有评论(0)