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
在面对各种场景时都更加健壮和高效。
尽管发生了这些内部变化,但 HashMap
的 API 完全保持不变,这是优秀软件设计的一个典范------在提升性能和质量的同时,保证了向后兼容性。
对于任何新的项目,都应优先使用 JDK 8 及以上版本,以享受这些自动的性能改进。