【某大厂一面】JDK1.8中对HashMap数据结构进行了哪些优化
在 JDK 1.8 中,HashMap
数据结构进行了重要的优化。相较于之前版本,JDK 1.8 引入了许多改进,提升了性能,尤其是在高负载的情况下。以下是 JDK 1.8 中 HashMap
数据结构的关键优化。
1. 链表转化为红黑树
在 JDK 1.8 之前,HashMap
使用链表来解决哈希冲突,即多个元素哈希值相同时,它们会被存储在同一个桶中,并通过链表(LinkedList
)来连接。这个设计虽然简单,但当哈希冲突非常严重时,链表的长度可能变得非常长,导致查找效率下降,最坏情况下,查找时间复杂度变为 O(n),这就违背了哈希表提供常数时间查找的优势。
JDK 1.8 优化:
- 红黑树(
TreeNode
)替代链表:当某个桶中的元素超过 8 个时,HashMap
会将链表转换成红黑树(TreeNode
)。红黑树是一种平衡二叉查找树,可以在 O(log n) 时间复杂度内完成查找、插入和删除操作。 - 转化条件:只有当桶中的元素超过 8 个,并且当前
HashMap
的总容量大于 64 时,才会进行链表转红黑树的操作。这是为了避免在哈希冲突不严重的情况下过度优化,影响性能。 - 恢复链表:如果红黑树中的元素数量降到 6 以下,
HashMap
会将其转换回链表,防止红黑树引入的额外复杂度影响性能。
优点:
- 高效查找:红黑树提供了对高负载情况下的查找操作进行优化,避免了链表退化为 O(n) 的情况。
- 动态调整:只有在真正需要的时候(即冲突非常严重时),才会使用红黑树,避免不必要的性能开销。
2. Node 类替代 Entry 类
在 JDK 1.8 中,HashMap
中的 Entry
类被 Node
类替代。这个变化本身并没有带来直接的性能提升,但它为进一步的优化和功能扩展提供了基础。
Node
类是一个新的内部类,它继承自Map.Entry
,表示HashMap
中的键值对。- 这种修改主要是为了与 JDK 1.8 中引入的其他改进(如并发处理、扩展性等)保持一致。
3. hash
方法优化
JDK 1.8 对 hash()
方法进行了优化。在 JDK 1.7 及之前,HashMap
计算元素的哈希值时,采用了 key.hashCode()
对元素哈希值进行扰动操作,并结合原始的哈希值来减少哈希冲突。但这种方法在某些情况下可能导致哈希冲突过于集中,影响性能。
优化:
- 扰动函数优化:JDK 1.8 对哈希值进行了更加复杂的扰动(扰动函数通过多次位运算来实现),以进一步减少冲突的概率。具体来说,
hash()
方法将键的原始哈希值与其高位进行异或操作,这样可以避免低位的哈希值集中,降低了哈希冲突的概率。
static final int hash(Object key) {
int h = key.hashCode();
h ^= (h >>> 16);
return h;
}
这种改进减少了哈希冲突,提高了哈希表的性能,尤其是在键的 hashCode()
分布不均的情况下。
4. resize
(扩容)操作优化
HashMap
的扩容是当元素数量达到阈值时,哈希表的容量会扩大一倍(从 threshold = capacity * loadFactor
计算得出)。在 JDK 1.7 及之前,扩容过程中会将原有的所有元素重新计算哈希值并重新映射到新的数组位置。
JDK 1.8 优化:
- 延迟扩容:在 JDK 1.8 中,当哈希表需要扩容时,
resize
操作做了优化,减小了数组的复制操作。新的扩容方式引入了 分段扩容(lazy expansion),即将resize
和元素搬迁过程异步处理,避免扩容过程中对性能产生过大的影响。 - 更低的扩容阈值:JDK 1.8 在扩容时会减少阈值,以更快地触发扩容操作,避免哈希表过度膨胀导致性能问题。
5. 并发支持的改进
尽管 HashMap
本身并不支持多线程并发写入(如 ConcurrentHashMap
),但 JDK 1.8 对 HashMap
的一些方法进行了优化,确保了在一定程度上的线程安全性:
computeIfAbsent
:JDK 1.8 引入了computeIfAbsent
方法,该方法可以在不需要显式同步的情况下确保元素只被计算一次。这是通过内置的synchronized
锁来实现的,尽管它无法保证并发写入时的线程安全,但在多线程环境下读操作时能有效提高效率。
6. forEach
和 removeIf
方法
JDK 1.8 引入了两个方法,分别是 forEach
和 removeIf
,它们使得 HashMap
的操作更加灵活和高效:
-
forEach
:这是一个新增的默认方法,它允许我们以更声明式的方式对HashMap
进行遍历。通过forEach
,可以减少手动创建迭代器的开销,使代码更加简洁。map.forEach((k, v) -> System.out.println(k + "=" + v));
-
removeIf
:这个方法用于根据给定条件移除元素,提供了更简洁、直接的 API 接口,简化了remove
操作。map.entrySet().removeIf(entry -> entry.getValue() == null);
7. putIfAbsent
和 replace
方法
这些方法在 JDK 1.8 中得到了强化,它们使得 HashMap
的写入操作更加高效且线程安全。
putIfAbsent
:该方法用于只有在键不存在时才进行插入操作,避免了传统的先containsKey
后put
的多次判断操作,提供了原子性。replace
:当映射中的某个键存在时,替换对应的值。
8. 总结 JDK 1.8 的 HashMap
优化
优化点 | 说明 |
---|---|
链表转红黑树 | 哈希冲突严重时,链表转为红黑树,查找、插入、删除复杂度由 O(n) 提升为 O(log n)。 |
hash 方法优化 | 通过更复杂的扰动函数减少哈希冲突,提高分布均匀性。 |
resize 操作优化 | 扩容时减少搬迁次数,降低性能损失。 |
并发支持改进 | 增加了 computeIfAbsent 方法,简化了多线程环境下的操作。 |
forEach 和 removeIf | 引入新的 API,提高操作效率和代码简洁度。 |
putIfAbsent 和 replace | 提供原子操作方法,避免重复判断,提高并发性能。 |
通过这些优化,JDK 1.8 的 HashMap
在高并发、冲突频繁的情况下提供了更好的性能,减少了在常见操作中的时间复杂度,尤其是在大量数据和高负载情况下。