一、前言
HashMap 是 Java 中用于“键值映射”的高性能容器,适用于快速存取、数据映射和缓存等多种场景,是开发中最常用的数据结构之一。
本文将通过源码,让你从底层到使用全方位掌握HashMap
的实现细节与设计考量。
本文是作者学习总结的文章,有错误的地方还请指出。
二、类结构与继承
2.1 类声明
继承自
AbstractMap
,实现了Map
、Cloneable
、Serializable
等接口
2.2 关键内部类
Node<K,V>
静态内部类,实现
Map.Entry
,用于链表节点存储。
TreeNode<K,V>
JDK 8 新增,继承自
Node
并添加红黑树,用于高碰撞场景下的树化。
三、底层数据结构与常量
3.1 核心字段
/**
* 哈希表主数组,存储键值对,首次使用时初始化。
*/
transient Node<K,V>[] table;
/**
* 缓存 entrySet() 结果。
*/
transient Set<Map.Entry<K,V>> entrySet;
/**
* 当前键值对数量。
*/
transient int size;
/**
* 结构修改次数,用于 fail-fast。
*/
transient int modCount;
/**
* 扩容阈值(容量 × 负载因子)。
*/
int threshold;
/**
* 负载因子,决定扩容时机。
*/
final float loadFactor;
3.2 重要常量
/**
* 默认初始容量,必须是 2 的幂,默认是 16。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/**
* 最大容量,构造函数传入更大值时会使用此上限,必须是 2 的幂,最大为 2^30。
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认负载因子,未指定时使用,默认是 0.75。
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* 链表转红黑树的阈值,单个桶中节点数 ≥ 8 时转换为树结构。
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 红黑树退化为链表的阈值,节点数 ≤ 6 时转换回链表结构。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 允许树化的最小哈希表容量,小于该值时即使桶中节点数达到 TREEIFY_THRESHOLD 也会先扩容而非树化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
四、哈希与索引计算
4.1 扰动函数
/**
* 计算键的哈希值,通过扰动函数(高位参与运算)来减少哈希冲突。
* 如果 key 为 null,则哈希值为 0。
*
* @param key 键
* @return 经过扰动处理后的哈希值
*/
static final int hash(Object key) {
int h;
// 使用高位扰动算法,防止哈希冲突集中在低位
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个方法的核心是通过
h ^ (h >>> 16)
这一扰动函数,把高位信息混合到低位,提高哈希值的随机性,从而使元素在数组中分布得更均匀,减少哈希冲突。
4.2 容量对齐与索引
/**
* 返回大于等于给定容量 cap 的最小的 2 的幂次方值。
* 例如,输入 10,返回 16。
*
* @param cap 期望的初始容量
* @return 返回最小的满足条件的 2 的幂次方容量
*/
static final int tableSizeFor(int cap) {
// 先减 1 是为了避免 cap 本身就是 2 的幂时多加一位
int n = cap - 1; // n = cap - 1 = 9
// 通过一系列位或运算将高位的1扩展到低位
// 初始值 n = 9 -> 0000 1001
n |= n >>> 1; //-> 0000 1001 | 0000 0100 = 0000 1101
n |= n >>> 2; //-> 0000 1101 | 0000 0011 = 0000 1111
n |= n >>> 4; //-> 0000 1111 | 0000 0000 = 0000 1111
n |= n >>> 8; //-> 0000 1111 | 0000 0000 = 0000 1111
n |= n >>> 16; //-> 0000 1111 | 0000 0000 = 0000 1111
// 最后 n = 15,返回 n + 1 = 16
// 若 n 小于 0,返回 1;若超过最大容量限制,返回最大容量;否则返回 n + 1(即下一个 2 的幂)
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
这个方法可以保证
HashMap
的内部数组长度始终是 2 的幂,便于通过位运算快速定位桶位置((n - 1) & hash)
,从而提升性能。
五、构造方法分析
// 指定初始容量和负载因子的构造方法
public HashMap(int initialCapacity, float loadFactor) {
// 初始容量不能小于0
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
// 如果初始容量大于最大容量,强制设为最大值
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
// 负载因子必须为正且非NaN
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
this.loadFactor = loadFactor;
// 根据初始容量计算阈值(容量为大于等于指定值的最小2的幂)
this.threshold = tableSizeFor(initialCapacity);
}
// 指定初始容量,使用默认负载因子(0.75)
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
// 无参构造方法,使用默认初始容量(16)和默认负载因子(0.75)
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // 其他字段默认初始化
}
// 根据已有 Map 创建一个新的 HashMap,复制其所有映射关系
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false); // 将参数 map 中的键值对放入当前 HashMap 中
}
构造器 | 功能说明 |
---|---|
HashMap(int, float) | 指定容量和负载因子 |
HashMap(int) | 指定容量,默认负载因子 |
HashMap() | 默认容量和负载因子 |
HashMap(Map<K, V>) | 复制另一个 Map 的内容 |
无参构造延迟到首次
put
时初始化table
,指定容量/因子可优化性能并减少扩容次数。
六、存储与扩容机制
6.1 putVal
流程
// 向 HashMap 中插入或更新一个键值对
public V put(K key, V value) {
// 计算 key 的扰动哈希值,并调用核心方法
return putVal(hash(key), key, value, false, true);
}
/**
* HashMap 的核心插入/更新逻辑
*
* @param hash key 的哈希值(已扰动处理)
* @param key 键
* @param value 值
* @param onlyIfAbsent 如果为 true,则已有值不替换
* @param evict 控制后续操作(如缓存删除),创建模式下为 false
* @return 旧值(如果替换了),否则返回 null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab;
Node<K,V> p;
int n, i;
// 1. 如果 table 还没初始化或长度为 0,先 resize() 分配初始数组
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算在数组中的槽位索引 i = (n-1) & hash
if ((p = tab[i = (n - 1) & hash]) == null) {
// 2.1 槽位为空,直接插入新节点
tab[i] = newNode(hash, key, value, null);
} else {
Node<K,V> e;
K k;
// 3. 如果头节点 key 相等,则准备更新
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
}
// 4. 如果该槽已经是红黑树结构,则调用树的插入逻辑
else if (p instanceof TreeNode) {
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
}
// 5. 否则遍历链表,查找或追加
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
// 5.1 到达链尾,追加新节点
p.next = newNode(hash, key, value, null);
// 5.2 如果链表长度超过阈值,触发树化
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 5.3 找到相同 key,退出循环准备更新
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
p = e;
}
}
// 6. 如果找到已有节点 e,则根据 onlyIfAbsent 决定是否替换 value,返回旧值
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 钩子方法,默认情况下不做任何操作,允许子类覆盖该方法,以支持扩展行为
afterNodeAccess(e);
return oldValue;
}
}
// 7. 插入新节点后,更新修改计数和 size;若超过阈值,执行扩容
++modCount;
if (++size > threshold)
resize();
// 钩子方法,默认情况下不做任何操作,允许子类覆盖该方法,以支持扩展行为
afterNodeInsertion(evict);
return null;
}
- 初始化
table
(若未分配)- 计算槽位索引并判断该位置是否为空
- 解决链表冲突、
链表→红黑树
转换、红黑树插入- 更新已有值或新增节点(末尾增加)
- 在插入后维护
modCount
、size
,并在超过threshold
时触发扩容
6.2 扩容 (resize
)流程
/**
* 初始化或扩展 HashMap 的内部数组,并将旧数据迁移到新数组中。
*
* @return 扩容或初始化后的 table 数组
*/
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1. 如果已有容量
if (oldCap > 0) {
// 1.1 达到最大容量,不再扩容
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 1.2 否则容量翻倍,阈值也翻倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1;
}
}
// 2. 第一次初始化:如果 threshold 已存放初始容量,则直接用之
else if (oldThr > 0) {
newCap = oldThr;
}
// 3. 默认初始化路径:初始容量为 DEFAULT_INITIAL_CAPACITY
else {
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4. 若 newThr 仍未计算,则按 loadFactor 计算
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY)
? (int)ft
: Integer.MAX_VALUE;
}
threshold = newThr;
// 5. 分配新数组并替换旧引用
@SuppressWarnings("unchecked")
Node<K,V>[] newTab = (Node<K,V>[]) new Node[newCap];
table = newTab;
// 6. 如果是扩容(oldTab 非空),则重新分配所有旧节点
if (oldTab != null) {
// 遍历旧数组每个槽
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e = oldTab[j];
if (e == null) continue;
// 释放旧引用 help GC
oldTab[j] = null;
// 6.1 单节点:直接放入新数组对应位置
if (e.next == null) {
int idx = e.hash & (newCap - 1);
newTab[idx] = e;
}
// 6.2 红黑树节点:调用 split 逻辑分裂到两边
else if (e instanceof TreeNode) {
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
}
// 6.3 普通链表:按 hash 高位分为低位链和高位链
else {
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
// 保存下一节点
next = e.next;
// 判断当前节点应留在原索引 (低位) 还是搬到原索引+oldCap (高位)
// 低位
if ((e.hash & oldCap) == 0) {
if (loTail == null) loHead = e;
else loTail.next = e;
loTail = e;
} else {
// 高位
if (hiTail == null) hiHead = e;
else hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 将低位链挂到 newTab[j]
if (loTail != null) {
// 断开链尾
loTail.next = null;
newTab[j] = loHead;
}
// 将高位链挂到 newTab[j + oldCap]
if (hiTail != null) {
// 断开链尾
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
return newTab;
}
- 计算新的容量和阈值
- 分配新数组并替换
- 将旧数据重新分配到新数组中,依据
hash
的高位进行划分(低位不变,高位映射到新位置)
七、读取与查找
// 根据 key 获取 value
public V get(Object key) {
Node<K,V> e;
// 调用 getNode 获取节点,如果不为空就返回其 value,否则返回 null
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
// 根据 key 和 hash 查找对应的 Node 节点
final Node<K,V> getNode(int hash, Object key) {
// Hash 表数组
Node<K,V>[] tab;
// 首节点和临时节点
Node<K,V> first, e;
// Hash 表长度
int n;
K k;
// 检查 table 是否初始化 且 长度大于0 且 该 hash 对应的 bucket 有值
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 检查首节点是否就是目标节点
if (first.hash == hash && ((k = first.key) == key || key.equals(k)))
return first;
// 若不是首节点,检查链表或红黑树中的后续节点
if ((e = first.next) != null) {
// 如果是红黑树节点,调用红黑树查找方法
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 遍历链表查找
do {
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e;
} while ((e = e.next) != null);
}
}
// 没找到返回 null
return null;
}
get()
会根据key
的hash
值快速定位到桶,然后根据hash
和equals
方法查找目标key
。桶中元素如果较少用链表,否则自动转换为红黑树查找。
八、删除操作
// 根据 key 删除对应的节点,并返回 value
public V remove(Object key) {
Node<K,V> e;
// 调用 removeNode 删除节点,返回被删除节点的 value
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 移除指定 key 对应的节点(键值对)
*
* @param hash key 的哈希值
* @param key 要移除的键
* @param value 仅当 matchValue 为 true 时才用于判断是否移除(一般传 null)
* @param matchValue 是否需要同时匹配 value(比如用于替换时的条件删除)
* @param movable 是否允许移动节点(在 resize 等场景中可能不允许移动)
* @return 被移除的节点,如果不存在则返回 null
*/
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; // Hash 表
Node<K,V> p; // 当前桶的首节点
int n, index;
// 如果 table 为空 或者 长度为 0 或 桶中首节点为空,说明 key 不存在
if ((tab = table) == null || (n = tab.length) == 0 ||
(p = tab[index = (n - 1) & hash]) == null)
return null;
// node 是目标节点,e 是临时变量
Node<K,V> node = null, e;
K k; V v;
// 检查首节点是否就是要删除的目标节点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
// 如果不是首节点,则遍历红黑树或链表查找目标节点
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
// 红黑树查找
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
node = e;
break;
}
// 记录上一个节点,用于链表删除操作
p = e;
} while ((e = e.next) != null);
}
}
// 如果没找到目标节点,返回 null
if (node == null) return null;
// 红黑树节点,调用红黑树删除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 删除的是首节点,直接替换为 node.next
else if (node == p)
tab[index] = node.next;
// 中间节点或尾节点,使用前驱节点 p 进行断链
else
p.next = node.next;
// 修改次数 +1,用于快速失败的迭代器
++modCount;
// 实际大小 -1
--size;
// 留给子类(如 LinkedHashMap)复写的方法
afterNodeRemoval(node);
return node;
}
九、碰撞处理与树化
- 链表方式:默认在桶尾插入新节点,性能退化时为
O(n)
- 树化条件:
单桶链表长度 ≥ TREEIFY_THRESHOLD (8) 且 table.length ≥ MIN_TREEIFY_CAPACITY (64)
,调用treeifyBin()
将链表转为红黑树 - 退树:当树节点个数降至
UNTREEIFY_THRESHOLD (6)
以下时,恢复链表
十、迭代器与 Fail‑Fast
10.1 什么是 Fail-Fast?
Fail-Fast
是一种快速失败机制:当多个线程对同一个集合进行结构性修改(非线程安全)时,若集合的迭代器检测到结构被改变,会立刻抛出异常,防止出现不一致状态或数据错误。
10.2 为什么需要 Fail-Fast?
因为
HashMap
不是线程安全的,如果一个线程正在遍历它,另一个线程修改了结构(如新增/删除元素),可能导致:
- 死循环
- 元素遗漏
- 数据不一致
为了避免这些严重问题,Java 设计了 Fail-Fast
机制,通过比较结构修改次数,一旦发现异常立刻中止操作。
10.3 modCount
与 expectedModCount
modCount
:记录结构性修改的次数expectedModCount
:当我们通过HashMap
的keySet()
、entrySet()
等方法创建迭代器时,迭代器内部会保存一份副本,期望的修改次数
10.4 如何触发 Fail-Fast?
- 当迭代器调用
next()
或remove()
的时候,都会校验- 只要
HashMap
的结构被其他线程修改(或者同一线程未通过迭代器的remove()
删除),modCount
就会变,但迭代器内部expectedModCount
没变,就会抛异常。
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
10.5 哪些操作会修改 modCount?
结构性修改(结构发生变化的操作)
put(K, V)
remove(K)
clear()
resize()
10.6 正确的删除方法
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
if ("a".equals(key)) {
// 不会触发 ConcurrentModificationException
it.remove();
}
}
十一、JDK 7 与 JDK 8 的主要变化
11.1 数据结构的变化
版本 | 底层结构 | 冲突解决 | 特性 |
---|---|---|---|
JDK 7 | 数组 + 链表 | 采用链表(拉链法)解决冲突 | 不支持树化 |
JDK 8 | 数组 + 链表 + 红黑树 | 冲突较多时链表转为红黑树 | 支持链表树化,提升查询效率 |
- JDK 7 中:所有冲突元素插入链表头部(头插法)。
- JDK 8 中:冲突元素插入链表尾部(尾插法),并引入红黑树结构。
11.2 Hash 函数优化(扰动函数)
- JDK 7:使用
key.hashCode()
后直接与桶长度取模(index = hashCode % length)
。 - JDK 8:引入 扰动函数
(hash spreading)
,通过高位与低位异或 + 右移,使hash
分布更均匀,解决hashCode()
不均匀时的桶偏斜问题。
11.3 树化机制(链表转红黑树)
- 触发条件:桶中元素个数 > 8(
TREEIFY_THRESHOLD
)且数组长度 >= 64(MIN_TREEIFY_CAPACITY
) - 好处:
- 链表查询时间复杂度:O(n)
- 红黑树查询时间复杂度:O(log n)
- 在高 hash 冲突场景下显著提升性能
11.4 并发安全改进(虽仍非线程安全)
项目 | JDK 7 | JDK 8 |
---|---|---|
插入方式 | 头插 | 尾插 |
扩容方式 | 扩容时将链表重新散列,顺序不变 | resize 时将链表拆成低位链和高位链,保持原顺序 |
并发扩容 | 多线程扩容可能形成死循环(链表成环) | 更安全稳定,避免死循环 |
十二、与其他 Map 实现的对比
特性 | HashMap | Hashtable | LinkedHashMap | ConcurrentHashMap |
---|---|---|---|---|
线程安全 | 否 | 是(同步) | 否 | 是(分段锁/CAS) |
允许 null 键/值 | 是 | 否 | 是 | 否 |
碰撞处理 | 链表/红黑树 | 链表 | 链表/红黑树 | 链表/红黑树 |
遍历顺序 | 不保证 | 不保证 | 插入/访问顺序 | 不保证 |
扩容因子 | 1.5× | 2× | 同 HashMap | 动态 |
十三、常见面试题
待定