HashMap 详解
HashMap 是 Java 集合框架中基于哈希表实现的键值对存储结构,允许 null
键和 null
值,非线程安全。以下是其核心知识点,适合面试准备:
1. 底层数据结构
-
数组 + 链表/红黑树(Java 8+)
-
数组(桶数组):默认初始容量为 16,存储链表或红黑树的头节点。
-
链表:哈希冲突时,键值对以链表形式存储。
-
红黑树(Java 8 优化):当链表长度 ≥8 且桶数组长度 ≥64 时,链表转为红黑树(查询时间复杂度从 O(n) → O(logn))。
-
2. 哈希函数与索引计算
-
哈希值计算:
// Java 8 的哈希扰动函数 static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
-
目的:通过高位异或减少哈希冲突(避免低位相同导致冲突)。
-
-
索引计算:
index = (n - 1) & hash; // n 为桶数组长度
-
条件:桶长度
n
必须为 2 的幂(通过按位与代替取模运算,提高效率)。
-
3. 核心参数
-
容量(Capacity):桶数组的长度,默认 16。
-
负载因子(Load Factor):默认 0.75,决定扩容阈值。
-
阈值(Threshold):
容量 × 负载因子
,当元素数量超过阈值时触发扩容。 -
树化阈值:链表长度 ≥8 且桶数组长度 ≥64 时,链表转为红黑树。
-
退化阈值:红黑树节点数 ≤6 时,退化为链表。
4. 扩容机制(Rehashing)
-
触发条件:元素数量 > 阈值。
-
扩容流程:
-
新容量 = 旧容量 × 2(保证容量为 2 的幂)。
-
创建新桶数组,并重新计算所有键值对的索引。
-
Java 8 优化:通过
hash & oldCap
判断元素是否需要迁移到高位(避免全量重新哈希)。
-
-
性能影响:扩容时需重新哈希所有元素,时间复杂度为 O(n),建议预分配容量以减少扩容次数。
5. 线程安全问题
-
问题表现:
-
数据覆盖:多线程同时插入时可能导致数据丢失。
-
死循环(Java 7 及之前):链表在扩容时可能形成环,导致
get()
死循环。
-
-
解决方案:
-
使用
ConcurrentHashMap
(分段锁或 CAS 机制)。 -
使用
Collections.synchronizedMap(new HashMap<>())
。
-
6. Java 8 的优化
-
红黑树替代链表:解决哈希冲突严重时链表查询效率低的问题。
-
扩容优化:利用高位异或判断元素迁移位置,减少重新哈希计算量。
-
遍历性能提升:使用
EntrySet
迭代器替代KeySet
,减少冗余计算。
7. 关键方法解析
(1) put(K key, V value)
-
计算键的哈希值,确定桶索引。
-
若桶为空,直接插入新节点。
-
若桶为链表/红黑树,遍历查找是否已存在相同键:
-
存在:更新值。
-
不存在:插入新节点,链表长度超过阈值则树化。
-
-
检查是否需扩容。
(2) get(Object key)
-
计算键的哈希值,定位桶索引。
-
遍历链表或红黑树,通过
equals()
查找匹配的键。
8. 常见面试题
Q1:HashMap 为什么线程不安全?
-
数据竞争:多线程同时修改可能导致数据丢失或死循环(Java 7 的链表头插法问题)。
-
解决方案:使用
ConcurrentHashMap
或同步包装类。
Q2:HashMap 与 HashTable 的区别?
特性 | HashMap | HashTable |
---|---|---|
线程安全 | 非线程安全 | 线程安全(方法同步) |
性能 | 高 | 低(同步开销) |
允许 null 键值 | 允许 | 不允许 |
迭代器 | 快速失败(Fail-Fast) | 未定义 |
Q3:负载因子为什么是 0.75?
-
权衡空间与时间:负载因子过小(如 0.5)会导致频繁扩容,浪费空间;过大(如 1.0)会增加哈希冲突概率。0.75 是经验值,平衡了时间和空间成本。
Q4:为什么容量必须是 2 的幂?
-
高效计算索引:
(n - 1) & hash
等效于hash % n
,但位运算更快。 -
均匀分布哈希值:减少哈希冲突。
Q5:如何设计自定义对象作为 HashMap 的键?
-
重写
hashCode()
和equals()
:-
hashCode()
应保证相同对象返回相同值,不同对象尽量分布均匀。 -
equals()
需严格判断对象相等性。
-
-
示例:
class Key { private int id; @Override public int hashCode() { return Objects.hash(id); } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Key key = (Key) obj; return id == key.id; } }
9. 使用建议
-
预分配初始容量:避免频繁扩容,例如
new HashMap<>(64)
。 -
避免频繁修改哈希码:作为键的对象应不可变(如
String
、Integer
)。 -
高并发场景:优先选择
ConcurrentHashMap
。
总结
HashMap 是 Java 开发的核心数据结构,理解其底层实现、扩容机制和线程安全问题对优化程序性能至关重要。在面试中,需重点阐述其数据结构、哈希冲突解决、Java 8 优化及线程安全解决方案。