1. 引言
HashMap 是 Java 开发中使用频率最高的集合类之一,也是面试中绕不开的核心考点。它基于哈希表实现,能够在理想情况下以 O(1) 的时间复杂度完成键值对的存取。然而,如果对它的底层原理理解不透彻,很容易在扩容、哈希冲突、并发修改等场景下踩坑。本文将从数据结构、哈希算法、扩容机制、JDK 8 优化以及实战注意事项几个维度,带你系统掌握 HashMap。
2. 底层数据结构
HashMap 的底层由「数组 + 链表 + 红黑树」组成。数组是主体,负责按哈希值定位桶;链表用于解决哈希冲突;当链表过长时,会转换为红黑树以提升查询效率。
- 数组(Node\[\] table):每个位置称为桶(bucket),默认初始容量为 16。
- 链表(Node):当多个键的哈希值映射到同一桶时,以链表形式串联,插入采用尾插法。
- 红黑树(TreeNode):当链表长度超过阈值(默认 8)且数组容量不小于 64 时,链表转为红黑树,将最坏时间复杂度从 O(n) 降为 O(log n)。
3. 哈希算法与索引定位
HashMap 并非直接使用 key 的 hashCode,而是先对 hashCode 做一次扰动处理,再与数组长度减一进行按位与运算,从而得到桶下标。
java
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这里将高 16 位与低 16 位做异或,目的是让高位信息也参与低位运算,从而在数组长度较小时尽量分散哈希值,减少碰撞。索引计算方式为 (n - 1) & hash,其中 n 为数组长度,且始终为 2 的幂。
4. 扩容机制
当 HashMap 中的元素数量超过「容量 × 负载因子」时,会触发扩容。默认负载因子为 0.75,即容量 16 时,元素达到 12 个就扩容。
- 扩容过程:容量翻倍,并重新计算每个元素的桶下标。
- JDK 8 优化:扩容后元素要么留在原索引,要么移动到「原索引 + 旧容量」的位置,判断依据是新增位是 0 还是 1,无需重新计算哈希。
- 树化与退化:扩容后若红黑树节点数少于 6,会退化为链表,避免树结构带来的额外开销。
5. JDK 8 的关键优化
相比 JDK 7,JDK 8 的 HashMap 在多个方面做了重要改进,显著提升了性能和安全性。
| 对比项 | JDK 7 | JDK 8 |
|---|---|---|
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 插入方式 | 头插法 | 尾插法 |
| 扩容后索引 | 重新计算哈希 | 按新增位判断原位置或原位置 + 旧容量 |
| 并发安全 | 扩容时可能形成环形链表 | 避免环形链表,但依然非线程安全 |
6. 实战注意事项
在实际开发中,合理使用 HashMap 能提升程序性能,但也要注意以下几点:
- 预估容量:如果已知数据规模,应在构造时指定初始容量,减少扩容带来的性能损耗。
- 自定义对象作 key:必须同时重写 hashCode 和 equals,且保证 hashCode 稳定,否则会导致元素无法正确存取。
- 避免并发修改:HashMap 非线程安全,多线程写入可能造成数据丢失或死循环,应改用 ConcurrentHashMap。
- 不要修改已放入的 key:放入后若修改 key 中参与 hashCode 计算的字段,会导致无法再通过原 key 找到该元素。
7. 总结
HashMap 的核心在于哈希表的设计:通过扰动函数分散哈希值,通过链表和红黑树解决冲突,通过扩容机制维持性能。理解这些原理,不仅能帮你写出更高效的代码,也能在面试中从容应对各类追问。建议结合源码阅读和实际调试,进一步加深对扩容、树化等细节的理解。