JDK 7 和 JDK 8 中的 HashMap 有什么不同?

JDK 7 vs JDK 8

HashMap 是 Java 中最常用的数据结构之一,其实现并非一成不变。JDK 8 对 HashMap 进行了一次重大重构,带来了显著的性能提升和功能优化。理解这些变化,是深入理解 Java 集合框架的关键。

一、核心结论:从"数组+链表"到"数组+链表+红黑树"

最根本的区别在于其底层数据结构的演进:

  • JDK 7 :完全基于 数组 + 链表 的实现。当发生哈希冲突时,总是将新元素插入到链表的头部(头插法)。
  • JDK 8 :主要基于 数组 + 链表 ,但当链表长度超过一定阈值(TREEIFY_THRESHOLD = 8)时,会将链表转换为红黑树 (Treeify)。同时,插入新元素时采用尾插法

这一核心变化解决了 JDK 7 HashMap 在极端情况下的性能缺陷,并引入了更现代的工程实现。

二、详细对比与解析

以下表格和详解列出了两者在各个方面的主要区别:

特性 JDK 7 中的 HashMap JDK 8 中的 HashMap
底层数据结构 数组 + 链表 数组 + 链表 + 红黑树
插入方式 头插法(将新节点插入链表头部) 尾插法(将新节点插入链表尾部)
哈希算法 较为复杂,多次位运算 简化,性能更高,扰动减少
扩容后重哈希 必须重新计算每个元素的新位置 优化:元素的新位置要么是原位置,要么是原位置+旧容量
key 为 null 的处理 单独使用 putForNullKey() 方法处理 整合到正常的 putVal() 方法中
1. 数据结构的优化:引入红黑树
  • JDK 7 的问题 :在极端情况下,如果多个 key 的 hashCode() 值相同,会导致大量的哈希冲突,使得链表变得非常长。此时,HashMap 的查询性能会从 O(1) 退化到 O(n) ,就像遍历一个链表一样,效率极低。

  • JDK 8 的解决方案 :引入了红黑树(一种自平衡的二叉查找树)。当链表的长度超过 8 数组(桶)的总容量大于 64 时,链表会转化为红黑树。这样,即使在最坏情况下,查询性能也能保持在 O(log n) ,极大地提升了抗风险能力。

    为什么要设定阈值 8?

    这是一个基于概率统计(泊松分布)的权衡。开发者认为,哈希码分布良好的情况下,链表长度达到 8 的概率极低(小于千万分之一)。因此,在绝大多数情况下,HashMap 都能享受链表的简单性,只在极少数极端情况下启用更复杂的红黑树,这是一种空间和时间的折衷方案。

2. 插入方式的改变:从头插法到尾插法
  • JDK 7:头插法
// 复制代码
void transfer(Entry[] newTable) {
    for (Entry<K,V> e : table) { // 遍历旧数组
        while(null != e) {
            Entry<K,V> next = e.next; // 记录下一个节点
            int i = indexFor(e.hash, newTable.length); // 计算新位置
            e.next = newTable[i]; // **关键:将当前节点的next指向新桶的头节点**
            newTable[i] = e;      // **将当前节点设为新桶的头节点**
            e = next;             // 处理下一个节点
        }
    }
}
  • 缺点 :在多线程扩容时,头插法会改变链表中元素的顺序,容易导致环形链表 的形成,进而引起 Infinite Loop 和 CPU 100% 的问题。虽然这是非线程安全导致的bug,但头插法是其诱因。
  • JDK 8:尾插法
    在扩容时,会保持链表中原有元素的顺序。这样即使在多线程环境下错误地进行了扩容,也不会形成环形链表(但依然可能产生数据覆盖等线程安全问题,只是避免了死循环)。这修复了一个著名的并发bug,但并不意味着HashMap变成了线程安全的。
3. 扩容机制的优化:更高效的重哈希

在扩容时(resize),需要将旧数组中的元素重新计算位置后放到新数组中(rehash)。

  • JDK 7 :对每个元素都使用新的数组长度重新计算其索引位置 indexFor()
  • JDK 8 :进行了巧妙的优化。由于扩容后数组大小是原来的2倍(2^n),元素的新位置要么是原位置(oldIndex) ,要么是原位置 + 旧容量(oldIndex + oldCapacity)
    它通过 (e.hash & oldCap) == 0 来判断元素的新位置。这只是一个位操作,效率远高于重新计算哈希,极大地提升了扩容时的性能。
4. 其他优化
  • 哈希计算简化 :JDK 8 简化了 hash() 函数的计算过程,减少了扰动次数,在哈希分布均匀的前提下提升了一点计算性能。
  • 方法整合 :JDK 8 将一些特定情况(如 null key)的处理逻辑整合到了主方法中,使得代码结构更清晰,但可读性可能有所降低。

三、总结与影响

方面 JDK 7 JDK 8 带来的好处
数据结构 数组+链表 数组+链表+红黑树 防止性能恶意退化,最坏情况下的查询效率从 O(n) 提升到 O(log n)
插入方式 头插法 尾插法 避免多线程扩容时出现环形链表(死循环),但依然非线程安全
扩容机制 全部重新计算哈希 高效位运算确定新位置 扩容性能显著提升,重哈希开销大大降低
总体性能 良好,但在冲突严重时和扩容时性能较差 稳定且高效 无论是正常使用还是应对极端情况,性能都更加可靠

最后! JDK 8 对 HashMap 的优化是一次非常成功的现代化改造。它通过引入红黑树 解决了性能瓶颈,通过尾插法 修复了知名的并发隐患,并通过优化的重哈希算法 提升了扩容效率。这些改变使得 HashMap 在面对各种场景时都更加健壮和高效。

尽管发生了这些内部变化,但 HashMapAPI 完全保持不变,这是优秀软件设计的一个典范------在提升性能和质量的同时,保证了向后兼容性。

对于任何新的项目,都应优先使用 JDK 8 及以上版本,以享受这些自动的性能改进。

相关推荐
canonical_entropy9 小时前
从同步范式到组合范式:作为双向/δ-lenses泛化的可逆计算理论
后端·低代码·领域驱动设计
Funcy10 小时前
XxlJob 源码分析06:任务执行流程(一)之调度器揭秘
后端
AAA修煤气灶刘哥11 小时前
数据库优化自救指南:从SQL祖传代码到分库分表的骚操作
数据库·后端·mysql
excel11 小时前
应用程序协议注册的原理与示例
前端·后端
ytadpole13 小时前
揭秘xxl-job:从高可用到调度一致性
java·后端
Moonbit13 小时前
MoonBit 三周年 | 用代码写就 AI 时代的语言答卷
后端·程序员·编程语言
菜鸟谢13 小时前
QEMU
后端
玉衡子13 小时前
六、深入理解JVM执行引擎
java·jvm
bobz96513 小时前
calico vxlan 默认不依赖 BGP EVPN 携带 VNI
后端
bobz96513 小时前
vxlan 和 vlan 的不同点
后端