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 及以上版本,以享受这些自动的性能改进。

相关推荐
聪明的笨猪猪2 分钟前
Java Redis “核心基础”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
稚辉君.MCA_P8_Java2 小时前
JVM第二课:一文讲透运行时数据区
jvm·数据库·后端·容器
奋斗的小monkey2 小时前
Spring Boot 3.x核心特性与性能优化实战
java·spring boot·微服务·性能优化·响应式编程
程序猿DD3 小时前
将 GPU 级性能带到企业级 Java:CUDA 集成实用指南
java·架构
一成码农3 小时前
JavaSE面向对象(上)
java
qq_574656253 小时前
java-代码随想录第66天|Floyd 算法、A * 算法精讲 (A star算法)
java·算法·leetcode·图论
我是好小孩3 小时前
【Android】六大设计原则
android·java·运维·服务器·设计模式
小霞在敲代码3 小时前
HashMap - 底层原理
java·hashmap
Elsa️7464 小时前
个人项目开发(1):使用Spring Secruity实现用户登录
java·后端·spring