什么是 HashMap
HashMap 是 Java 集合框架中基于哈希表实现的 Map 接口,它提供了键值对的存储和检索功能。HashMap 允许使用 null 键和 null 值,并且不保证映射的顺序,特别是不保证该顺序恒久不变。
底层数据结构
JDK 7 及之前
- 数组 + 链表的组合实现
- 数组被称为哈希表的 "桶"(Bucket)
- 每个数组元素是一个链表的头节点
- 当发生哈希冲突时,将元素添加到链表的末尾
JDK 8 及之后
- 数组 + 链表 + 红黑树的组合实现
- 当链表长度超过阈值 (默认 8) 时,将链表转换为红黑树
- 当红黑树节点数量少于阈值 (默认 6) 时,将红黑树转回链表
- 这样的设计在数据量大时能提高查询效率
工作原理
存储过程
- 计算哈希值 :通过
hash(key)方法计算键的哈希值 - 确定桶位置 :使用
(n-1) & hash计算键在数组中的索引位置 - 处理冲突 :
- 如果桶为空,直接存储
- 如果桶不为空,检查是否为同一元素,是则替换值
- 否则添加到链表或红黑树中
哈希函数实现
java
运行
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个实现将哈希码的高 16 位与低 16 位进行异或运算,减少哈希冲突的概率。
重要参数
- 初始容量 (Initial Capacity):默认 16,必须是 2 的幂
- 负载因子 (Load Factor):默认 0.75,当元素数量达到容量 * 负载因子时进行扩容
- 扩容阈值 (Threshold):容量 * 负载因子,达到此值时进行扩容
- 树化阈值 (TREEIFY_THRESHOLD):默认 8,链表转红黑树的阈值
- 反树化阈值 (UNTREEIFY_THRESHOLD):默认 6,红黑树转链表的阈值
扩容机制
- 当元素数量达到扩容阈值时,HashMap 会扩容到原来的 2 倍
- 重新计算每个元素的哈希值和桶位置
- 将元素重新分配到新的桶中
- 扩容过程会消耗较多资源,因此建议在初始化时指定合适的容量
常用方法
put(K key, V value):添加键值对get(Object key):根据键获取值remove(Object key):根据键删除键值对containsKey(Object key):判断是否包含指定键containsValue(Object value):判断是否包含指定值size():返回键值对数量clear():清空所有键值对
特点与性能
优点
- 快速访问:平均时间复杂度为 O (1)
- 灵活性:允许 null 键和 null 值
- 高效插入删除:在哈希分布均匀的情况下性能优异
缺点
- 无序性:不保证元素的存储顺序
- 线程不安全:多线程环境下可能出现并发问题
- 哈希冲突:可能导致性能下降
线程安全性
HashMap 是非线程安全的,在多线程环境下可能出现以下问题:
- 扩容时可能形成环形链表,导致死循环
- 数据覆盖问题
- 迭代时抛出 ConcurrentModificationException
解决方法:
- 使用
Collections.synchronizedMap()包装 - 使用
ConcurrentHashMap(推荐) - 在关键代码块使用
synchronized关键字
与其他 Map 实现类的区别
| 实现类 | 底层结构 | 线程安全 | 有序性 | 允许 null |
|---|---|---|---|---|
| HashMap | 数组 + 链表 + 红黑树 | 否 | 否 | 是 |
| TreeMap | 红黑树 | 否 | 是 (自然排序) | 否 |
| LinkedHashMap | 链表 + 哈希表 | 否 | 是 (插入顺序) | 是 |
| Hashtable | 数组 + 链表 | 是 | 否 | 否 |
| ConcurrentHashMap | 分段锁 / CAS | 是 | 否 | 否 |
使用建议
- 初始化容量:根据预期数据量设置合适的初始容量
- 负载因子:一般使用默认值 0.75,特殊情况可调整
- 键的选择:尽量使用不可变对象作为键
- 线程安全:多线程环境下使用 ConcurrentHashMap
- 遍历方式:根据需要选择 keySet ()、values () 或 entrySet () 遍历
HashMap 是 Java 中最常用的数据结构之一,理解其底层实现和工作原理对于编写高效的 Java 程序至关重要。