以下是关于 HashMap 的常见面试题及其答案整理,涵盖底层原理、使用场景和优化技巧
1. HashMap 的底层数据结构是什么?
- 答案:
JDK 1.8 之前:数组 + 链表(链表解决哈希冲突)。
JDK 1.8 及之后:数组 + 链表/红黑树(当链表长度 ≥8 且数组长度 ≥64 时,链表转为红黑树,提高查询效率)。
2. HashMap 的工作原理(put/get 过程)?
put 过程:
- 计算键的哈希值:
hash(key) = (key == null) ? 0 : key.hashCode() ^ (hashCode >>> 16)
(扰动函数减少哈希冲突)。 - 根据哈希值确定数组下标:
index = (n - 1) & hash
(n
为数组长度)。 若该位置为空,直接插入键值对;否则遍历链表/红黑树:
- 若键已存在,更新值;
- 若键不存在,插入到链表尾部(或红黑树)。
- 插入后判断是否需要扩容。
get 过程:
- 计算键的哈希值,确定数组下标。
- 遍历链表/红黑树,通过
equals()
方法匹配键,返回对应的值。
3. 为什么 HashMap 线程不安全?
原因:
- 多线程扩容:扩容时链表可能形成环形结构,导致
get()
死循环(JDK 1.7 问题,1.8 已优化)。 - 数据覆盖:多线程同时插入且哈希冲突时,可能覆盖已存在的键值对。
- 可见性问题:未使用同步机制,修改对其他线程不可见。
- 多线程扩容:扩容时链表可能形成环形结构,导致
4. HashMap 的哈希冲突如何解决?
- 拉链法(链地址法):将冲突的键值对存储在链表或红黑树中。
- 开放寻址法:HashMap 未使用,但其他类(如
ThreadLocal
)可能采用。
5. 为什么链表长度超过 8 会转为红黑树?
- 设计依据:根据泊松分布,哈希冲突时链表长度达到 8 的概率极低(约 1/千万次),此时转为红黑树可平衡查询效率与树化成本。
- 退化条件:当红黑树节点数 ≤6 时,退化为链表。
6. HashMap 的初始容量、负载因子与扩容机制?
- 默认初始容量:16。
- 负载因子(Load Factor):默认 0.75(平衡时间与空间开销)。
- 扩容条件:当元素数量 > 容量 × 负载因子时,扩容为原来的 2 倍。
扩容过程:
- 创建新数组(容量翻倍)。
- 重新计算键的哈希并分配到新位置(JDK 1.8 优化:链表节点保持原顺序,避免环形链表)。
7. HashMap 允许键/值为 null,而 ConcurrentHashMap 不允许?
- HashMap:设计为单线程使用,允许 null 键值简化代码逻辑(例如表示“无值”)。
- ConcurrentHashMap:多线程场景下,null 可能导致歧义(无法区分“键不存在”和“键存在但值为 null”)。
8. HashMap 与 Hashtable 的区别?
特性 | HashMap | Hashtable |
---|---|---|
线程安全 | 否 | 是(方法用 synchronized 修饰) |
null 键/值 | 允许 | 禁止 |
初始容量 | 16 | 11 |
扩容机制 | 2 倍 | 2 倍 +1 |
哈希计算 | 扰动函数优化哈希分布 | 直接使用 key.hashCode() |
9. 如何设计一个高效的键类型(如自定义对象作为 Key)?
- 重写
hashCode()
和equals()
:确保哈希分布均匀且相等对象返回相同哈希。 示例:
@Override public int hashCode() { return Objects.hash(id, name); // 使用工具类生成复合哈希 } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; MyKey other = (MyKey) obj; return id == other.id && Objects.equals(name, other.name); }
10. 如何解决 HashMap 的多线程问题?
方案:
- 使用
ConcurrentHashMap
:分段锁(JDK 1.7)或 CAS + synchronized(JDK 1.8+)。 Collections.synchronizedMap()
:包装 HashMap,但性能较低。- 避免在多线程中直接操作 HashMap。
- 使用
11. HashMap 的遍历方式有哪些?
EntrySet 遍历:效率最高(直接获取键值对)。
for (Map.Entry<String, Integer> entry : map.entrySet()) { String key = entry.getKey(); int value = entry.getValue(); }
- KeySet 遍历:需额外调用
get()
,可能触发多次哈希计算。 Lambda 表达式(Java 8+):
map.forEach((k, v) -> System.out.println(k + ": " + v));
总结
- 数据结构与红黑树转换。
- 哈希冲突解决与扩容机制。
- 线程安全与替代方案。
- hashCode() 与 equals() 的设计。
- JDK 1.7 与 1.8 的差异(如头插法改尾插法、树化优化)。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。