HashMap数据结构深挖(五)之数据插入详讲
1. 概述
HashMap是Java集合框架中常用的数据结构,基于哈希表实现,提供了高效的键值对存储和查询功能。本文将详细剖析HashMap的数据插入过程,包括源码分析和关键步骤解释。
2. 数据插入流程
HashMap的数据插入主要通过put(K key, V value)
方法完成。以下是其核心流程:
2.1 put
方法入口
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
- 参数说明:
key
:键对象。value
:值对象。
- 返回值:如果键已存在,返回旧值;否则返回
null
。
2.2 putVal
方法核心逻辑
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. 检查哈希表是否为空或长度为0,若是则初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2. 计算索引位置,并检查该位置是否为空
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 直接插入新节点
else {
Node<K,V> e; K k;
// 3. 检查首节点是否匹配
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 键已存在,记录旧节点
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 红黑树插入
else {
// 4. 遍历链表查找匹配的节点
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null); // 插入到链表尾部
if (binCount >= TREEIFY_THRESHOLD - 1) // 检查是否需要树化
treeifyBin(tab, hash);
break;
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break; // 键已存在,记录旧节点
p = e;
}
}
// 5. 处理键已存在的情况
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value; // 更新值
afterNodeAccess(e);
return oldValue;
}
}
modCount++;
// 6. 检查是否需要扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.3 关键步骤说明
- 初始化哈希表:如果哈希表为空,调用
resize()
方法初始化。 - 计算索引位置:通过
(n - 1) & hash
计算键值对的存储位置。 - 处理哈希冲突:
- 如果首节点匹配,直接更新值。
- 如果是红黑树节点,调用
putTreeVal
方法插入。 - 如果是链表,遍历查找匹配的节点或插入到链表尾部。
- 扩容检查:如果键值对数量超过阈值,调用
resize()
方法扩容。
3 判断是否需要扩容
在插入数据之前,需要检查HashMap的底层数组(table
)是否为空或者长度为0。如果是,则需要进行扩容操作:
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
table
: HashMap的底层数组,用于存储键值对。resize()
: 扩容方法,会重新分配数组大小并重新计算哈希值。n
: 扩容后的数组长度。
4 哈希值的扰动
在HashMap中,插入数据的第一步是对键(key)的哈希值进行扰动处理。这是为了减少哈希冲突的概率。扰动处理的代码如下:
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
key.hashCode()
: 调用键的hashCode()
方法获取原始哈希值。h >>> 16
: 将哈希值右移16位,相当于将高16位移动到低16位。^
: 异或操作,将原始哈希值的高16位和低16位进行混合,增加哈希值的随机性。key == null
: 如果键为null
,则哈希值为0,HashMap允许键为null
。
5 计算下标并插入数据
根据扰动后的哈希值计算数据在数组中的下标:
tab[i = (n - 1) & hash]
n - 1
: 数组长度减1,用于与哈希值进行按位与操作,确保下标在数组范围内。&
: 按位与操作,相当于取模运算,但效率更高。
如果对应下标的位置为空,则直接插入数据;否则需要处理哈希冲突。
6 处理哈希冲突
如果目标位置不为空,则需要判断该位置是链表节点还是树节点:
6.1 链表节点
如果是链表节点,则遍历链表:
// 处理哈希冲突时的三种情况:键已存在、树节点、链表遍历
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
// 情况1:当前节点p的键与插入键完全匹配(哈希值相同且equals为true)
// 将p赋值给临时变量e,后续将用新值覆盖旧值
e = p;
else if (p instanceof TreeNode)
// 情况2:当前节点是红黑树节点
// 调用红黑树的插入方法,返回可能存在的重复节点e(若存在则覆盖值)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 情况3:当前是链表结构,开始遍历链表
for (int binCount = 0; ;++binCount) {
// 子情况3.1:到达链表尾部仍未找到相同键
// 创建新节点并插入链表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 判断链表长度是否达到树化阈值(TREEIFY_THRESHOLD=8,binCount从0计数,故>=7时触发)
if (binCount >= TREEIFY_THRESHOLD - 1)
// 将链表转换为红黑树(需满足数组长度≥MIN_TREEIFY_CAPACITY=64)
treeifyBin(tab, hash);
break;// 结束链表遍历
}
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
// 子情况3.2:在链表中找到相同键的节点e,跳出循环后续覆盖值
break;
// 指针后移,继续遍历链表
p = e;
}
}
p
: 当前节点。e
: 临时节点,用于遍历链表。TREEIFY_THRESHOLD
: 链表转换为红黑树的阈值,默认为8。treeifyBin
: 将链表转换为红黑树的方法。链表插入数据关键步骤说明
哈希冲突处理逻辑
①该段代码是 HashMap 解决哈希冲突的核心逻辑,分为三种情况处理(键重复、树节点插入、链表遍历插入)
②p.hash == hash 优先比较哈希值,避免频繁调用 equals() 提升性能
树化条件
①链表长度≥8(TREEIFY_THRESHOLD)且数组长度≥64时才会树化,否则仅扩容数组
②树化操作通过 treeifyBin()
实现,涉及红黑树重构
链表遍历优化
①binCount 统计链表长度(从0开始),循环中通过 p = e 实现指针后移
②插入新节点时采用尾插法(JDK 8 改进,以前是头插法这有可能导致多线程扩容死循环)
重复键处理
无论链表还是树结构,发现重复键时都会通过 e.value = value
覆盖旧值(由外层代码控制)
6.2 树节点
如果是树节点,则调用树节点的插入方法:
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
putTreeVal
: 向红黑树中插入节点的方法。
7 判断是否需要扩容
在插入完成后,需要检查HashMap的元素数量是否超过阈值(threshold
),如果超过则进行扩容:
if (++size > threshold)
resize();
size
: HashMap中键值对的数量。threshold
: 扩容阈值,等于容量乘以负载因子。
8 总结
HashMap的数据插入过程涉及哈希值的扰动、数组扩容、哈希冲突处理等多个步骤。通过扰动哈希值减少冲突,通过链表和红黑树处理冲突,最终保证HashMap的高效性。