1. 底层数据结构
- 数组 + 链表/红黑树:
HashMap
内部维护一个 桶数组(Node[] table),每个桶(Bucket)存储链表或红黑树的头节点。transient Node<K,V>[] table; // 桶数组 static class Node<K,V> implements Map.Entry<K,V> { final int hash; // 键的哈希值 final K key; V value; Node<K,V> next; // 链表指针 }
- 链表:哈希冲突时,冲突的键值对以链表形式存储。
- 红黑树(JDK 8+):当链表长度超过 8 时,链表转换为红黑树(优化查询效率);当节点数减少到 6 时,树退化为链表。
2. 哈希计算与索引定位
- 哈希扰动:
对键的hashCode()
进行二次哈希(高16位异或低16位),以减少哈希碰撞概率。static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
- 定位桶索引:
根据哈希值计算桶的位置,公式为hash & (table.length - 1)
(要求table.length
为 2 的幂)。
3. 插入键值对(put(K key, V value)
)
- 计算哈希值:调用
hash(key)
获取哈希值。 - 定位桶索引:通过哈希值确定键值对应存储的桶。
- 处理冲突:
- 桶为空:直接创建新节点存入桶中。
- 桶非空:遍历链表或树,检查是否存在相同键(
equals()
判断):- 存在:更新值。
- 不存在:插入新节点到链表/树末尾。
- 树化检查:若链表长度 ≥8,则转换为红黑树。
- 扩容检查:若元素总数 ≥
容量 × 负载因子
,触发扩容。
4. 查找值(get(Object key)
)
- 计算哈希值:同
put()
方法。 - 定位桶索引:找到对应桶。
- 遍历链表/树:通过
equals()
方法匹配键,返回对应值。若未找到,返回null
。
5. 删除键值对(remove(Object key)
)
- 定位桶和节点:类似
get()
过程找到目标节点。 - 移除节点:若为链表节点,调整链表指针;若为树节点,调整树结构。
- 退化检查:若树节点数 ≤6,退化为链表。
6. 扩容机制(Rehashing)
- 触发条件:当元素数量 ≥
容量 × 负载因子
(默认容量 16,负载因子 0.75)。 - 扩容过程:
- 新容量为旧容量的 2 倍(保证为 2 的幂)。
- 遍历所有节点,重新计算索引
hash & (newCapacity - 1)
。 - 数据迁移到新桶数组。
- 优化设计:
- 扩容后节点的新位置为 原位置 或 原位置 + 旧容量(利用哈希值的高位特征)。
- 避免全量重新哈希,提升效率。
7. 关键参数与性能
参数 | 默认值 | 作用 |
---|---|---|
初始容量 | 16 | 桶数组的初始大小 |
负载因子(loadFactor) | 0.75 | 触发扩容的阈值比例(空间与时间的权衡) |
树化阈值(TREEIFY_THRESHOLD) | 8 | 链表转换为红黑树的长度阈值 |
退化阈值(UNTREEIFY_THRESHOLD) | 6 | 红黑树退化为链表的节点数阈值 |
- 时间复杂度(平均情况):
- 插入、删除、查找:O(1)
- 最坏情况(全冲突):链表 O(n),红黑树 O(log n)。
8. 处理 null
键
HashMap
允许 一个null
键,其哈希值固定为 0,存储在索引 0 的桶中。- 查找时,需显式检查
key == null
的情况。
9. 线程安全问题
- 非线程安全:多线程并发修改可能导致数据不一致或死循环(JDK 7 及之前扩容时链表反转导致)。
- 解决方案:使用
ConcurrentHashMap
或Collections.synchronizedMap()
。
10. 示例代码
HashMap<String, Integer> map = new HashMap<>();
map.put("Apple", 10);
map.put("Banana", 5);
// 查找
System.out.println(map.get("Apple")); // 输出 10
// 删除
map.remove("Banana");
总结
HashMap
的高效性源于:
- 哈希算法优化:扰动函数减少哈希冲突。
- 动态扩容:平衡空间占用与操作效率。
- 冲突处理:链表与红黑树的灵活切换。
- 设计取舍:通过负载因子和树化阈值优化不同场景下的性能。