HashMap 详解
一、概述
HashMap 是 Java 集合框架中 java.util 包下的重要实现类,基于哈希表实现,用于存储键值对(Key-Value)映射。它允许 null 键和 null 值,是非线程安全的集合,其迭代器是快速失败(fail-fast)的。
核心特点:
-
存储键值对,键唯一(通过
equals()判断) -
无序(存储顺序与插入顺序不一致)
-
非线程安全
-
可以有null值
-
查询、插入、删除效率高(平均 O (1) 时间复杂度)
二、数据结构
1. 整体结构
HashMap 的底层数据结构在 JDK 1.7 和 JDK 1.8 中有差异:
-
JDK 1.7:数组 + 链表(拉链法解决哈希冲突)
-
JDK 1.8:数组 + 链表 + 红黑树(当链表长度超过阈值时转为红黑树,优化查询效率)
2. 各部分作用
-
数组(哈希表):也称为桶(Bucket),是 HashMap 的主体,默认初始容量为 16,元素类型为
Node<K,V>(JDK 1.8 中链表节点)或TreeNode<K,V>(红黑树节点)。 -
链表:解决哈希冲突(不同 key 计算出相同哈希值时,将元素以链表形式存储在同一桶中)。
-
红黑树:当链表长度超过 8 且数组容量 ≥ 64 时,链表会转为红黑树(平衡二叉树),将查询时间复杂度从 O (n) 优化为 O (log n)。
三、核心参数
| 参数 | 作用 | 默认值 |
|---|---|---|
initialCapacity | 初始容量(数组长度),必须是 2 的幂次 | 16 |
loadFactor | 负载因子(扩容阈值因子) | 0.75f |
threshold | 扩容阈值(capacity * loadFactor),当元素数量超过此值时触发扩容 | 12(16*0.75) |
size | 实际存储的键值对数量 | 0 |
DEFAULT_INITIAL_CAPACITY | 默认初始容量 | 16 |
MAXIMUM_CAPACITY | 最大容量(2^30) | 1073741824 |
TREEIFY_THRESHOLD | 链表转红黑树的阈值 | 8 |
UNTREEIFY_THRESHOLD | 红黑树转链表的阈值(删除元素时触发) | 6 |
MIN_TREEIFY_CAPACITY | 链表转红黑树的最小数组容量(避免频繁扩容) | 64 |
四、常用方法详解
1. put(K key, V value)
作用:向 HashMap 中添加键值对,若 key 已存在则覆盖旧值。
流程:
-
计算 key 的哈希值(通过hash(key)方法:
static final int hash(Object key) { int h; // 对 key 的 hashCode 进行高位异或低位,减少哈希冲突 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } -
根据哈希值计算数组索引(桶位置):
index = (n - 1) & hash(n 为数组长度,利用位运算替代取模,效率更高)。 -
检查桶位置:
-
若桶为空,直接创建新节点放入。
-
若桶不为空:
-
若头节点 key 与插入 key 相同(
equals()判断),覆盖旧值。 -
若头节点是红黑树节点,调用红黑树的插入方法。
-
若头节点是链表节点,遍历链表:
-
找到相同 key 则覆盖旧值。
-
未找到则在链表尾部插入新节点,插入后若链表长度 ≥ 8,且数组容量 ≥ 64,则将链表转为红黑树。
-
-
-
-
插入后若
size > threshold,触发扩容(resize())。
2. get(Object key)
作用:根据 key 获取对应 value,若 key 不存在则返回 null。
流程:
-
计算 key 的哈希值(同
put方法)。 -
根据哈希值计算数组索引,定位到目标桶。
-
检查桶中元素:
-
若桶为空,返回 null。
-
若桶中是红黑树,调用红黑树的查找方法。
-
若桶中是链表,遍历链表,通过
equals()匹配 key,找到则返回对应 value,否则返回 null。
-
3. containsKey(Object key)
作用:判断 HashMap 中是否包含指定 key。
流程:与 get 方法类似,区别在于找到 key 后返回 true,未找到则返回 false(不关心 value)。
4. getOrDefault(Object key, V defaultValue)
作用:根据指定的key获取对应的value,若key不存在则返回预设的默认值。
流程:调用get(key)获取value,判断value以及key的存在性,使用containKey是因为get(key)返回null有可能是key不存在也可能是key存在但是value为null,获取到value的话存到变量v中,不存在则返回defaultValue。
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
5. 其他常用方法
-
remove(Object key):删除指定 key 对应的键值对,返回被删除的 value(若 key 不存在返回 null)。 -
size():返回键值对数量。 -
isEmpty():判断是否为空(size == 0时返回 true)。 -
clear():清空所有键值对。 -
keySet():返回所有 key 的集合(Set<K>)。 -
values():返回所有 value 的集合(Collection<V>)。
五、扩容机制(resize())
1. 触发条件
当 HashMap 中键值对数量 size > threshold(capacity * loadFactor)时,触发扩容。
2. 扩容流程
-
计算新容量:新容量 = 旧容量 * 2(保证容量始终是 2 的幂次)。
-
计算新阈值:新阈值 = 新容量 * loadFactor。
-
创建新数组(容量为新容量)。
-
将旧数组中的元素迁移到新数组:
-
JDK 1.7:重新计算所有元素的哈希值和索引,再迁移(效率低)。
-
JDK 1.8:优化迁移逻辑:
-
对于链表节点:通过
(hash & oldCap) == 0
判断索引是否变化(无需重新计算哈希):
-
若为 0,索引不变,仍在原位置。
-
若为 1,新索引 = 旧索引 + 旧容量。
-
-
对于红黑树节点:拆分红黑树为两个链表(或红黑树),再迁移。
-
-
-
替换旧数组为新数组,更新容量和阈值。
六、常见面试题
1. HashMap 与 Hashtable 的区别?
| 对比项 | HashMap | Hashtable |
|---|---|---|
| 线程安全 | 非线程安全 | 线程安全(方法加 synchronized) |
| null 键值 | 允许 null 键和 null 值 | 不允许 null 键和 null 值 |
| 初始容量 | 16,扩容为 2 倍 | 11,扩容为 2 倍 + 1 |
| 效率 | 高(无同步开销) | 低(同步开销大) |
| 父类 | 继承 AbstractMap | 继承 Dictionary |
2. JDK 1.7 与 JDK 1.8 中 HashMap 的区别?
| 对比项 | JDK 1.7 | JDK 1.8 |
|---|---|---|
| 数据结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
| 链表插入方式 | 头插法(可能导致扩容时链表环化) | 尾插法(避免环化) |
| 扩容时哈希计算 | 重新计算哈希值 | 利用 (hash & oldCap) 判断索引 |
| 红黑树优化 | 无 | 有(链表转红黑树提升查询效率) |
| 冲突解决 | 拉链法 | 拉链法 + 红黑树 |
3. 为什么 HashMap 的初始容量是 16?
-
16 是 2 的幂次,通过
(n - 1) & hash计算索引时,n - 1的二进制全为 1(如 15 是1111),能保证哈希值的低位全参与运算,减少哈希冲突。 -
16 是平衡初始空间和扩容频率的折中值(太小易频繁扩容,太大浪费空间)。
4. 为什么负载因子是 0.75?
-
负载因子是「空间」与「时间」的平衡:
-
太小(如 0.5):哈希冲突少,但扩容频繁,浪费空间。
-
太大(如 1.0):空间利用率高,但哈希冲突多,查询效率低。
-
-
0.75 是基于概率统计的最优值(哈希冲突概率较低,且空间利用率合理)。
5. HashMap 为什么线程不安全?
-
JDK 1.7 中,扩容时链表头插法可能导致链表环化,引发死循环。
-
多线程并发修改时,可能出现数据覆盖(如同时
put相同 key 导致值丢失)。 -
解决方案:使用
ConcurrentHashMap(线程安全,效率高于 Hashtable)。
6. 哈希冲突的解决方法有哪些?
-
拉链法(HashMap 采用):将哈希值相同的元素以链表 / 红黑树形式存储在同一桶中。
-
开放地址法:当哈希冲突时,通过线性探测、二次探测等方式寻找下一个空桶。
-
再哈希法:使用多个哈希函数,若第一个函数冲突则使用第二个。
7. 红黑树的转换条件是什么?
-
链表转红黑树:当链表长度 ≥
TREEIFY_THRESHOLD(8),且数组容量 ≥MIN_TREEIFY_CAPACITY(64)。 -
红黑树转链表:当红黑树节点数量 ≤
UNTREEIFY_THRESHOLD(6)。
8. HashMap 的 key 可以是自定义对象吗?需要注意什么?
-
可以,但必须重写hashCode()和equals()
方法:
-
hashCode():保证相同对象返回相同哈希值,不同对象尽量返回不同哈希值(减少冲突)。 -
equals():保证相同对象返回 true(需满足自反性、对称性、传递性)。
-
-
注意:key 应尽量不可变(如 String、Integer),否则修改后可能导致无法找到对应 value。
七、总结
HashMap 是 Java 中最常用的键值对存储工具,其核心是通过哈希表实现高效的增删改查。JDK 1.8 引入红黑树优化长链表查询,使性能更稳定。理解其数据结构、扩容机制和线程安全性,对日常开发和面试都至关重要。在并发场景中,应优先使用 ConcurrentHashMap 替代 HashMap。
662

被折叠的 条评论
为什么被折叠?



