HashMap的put方法的具体流程?
HashMap 的 put 方法是其核心功能之一,用于将键值对存储到哈希表中。以下是 put 方法的详细流程(基于 JDK 1.8):
1. 核心流程概述
- 计算键的哈希值 → 定位桶(数组下标) → 处理哈希冲突 → 插入或更新值 → 扩容检查。
2. 详细步骤解析
步骤 1:计算键的哈希值
- 使用
key.hashCode()获取原始哈希值。 - 扰动函数(二次哈希):通过
(h = key.hashCode()) ^ (h >>> 16)对哈希值进行扰动,目的是减少哈希冲突。▼java复制代码static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }- 高位与低位异或,使哈希值的高位特征参与桶下标计算。
步骤 2:定位桶(数组下标)
- 通过
(n - 1) & hash计算桶下标(n是当前数组长度)。n是 2 的幂次,因此(n - 1) & hash等效于hash % n,但位运算效率更高。
步骤 3:处理哈希冲突
-
情况 1:桶为空(无冲突)
直接创建新节点(Node<K,V>)并存入该桶。 -
情况 2:桶非空(哈希冲突)
-
链表处理:遍历链表,依次检查节点的
key是否与当前key相等(通过equals方法)。- 若存在相同
key,更新对应的value。 - 若遍历到链表尾部仍未找到,创建新节点并插入尾部(JDK 1.8 尾插法)。
- 插入后,若链表长度 ≥ 8,触发链表转红黑树(需满足数组长度 ≥ 64,否则优先扩容)。
- 若存在相同
-
红黑树处理:若当前桶是红黑树节点(
TreeNode<K,V>),调用红黑树的插入逻辑。
-
步骤 4:检查扩容
- 触发条件:当键值对数量
size超过阈值threshold(capacity * loadFactor,默认负载因子 0.75)。 - 扩容操作:
- 数组长度扩容为原来的 2 倍(
newCap = oldCap << 1)。 - 重新计算所有键值对的桶下标,并迁移到新数组(通过高位判断:
(e.hash & oldCap) == 0)。- 若为 0:节点留在原位置。
- 若为 1:节点迁移到
原下标 + oldCap的位置。
- 数组长度扩容为原来的 2 倍(
3. 关键流程图解
▼plaintext复制代码put(key, value) ↓ 计算 hash(key) → 扰动哈希值 ↓ 定位桶下标:(n-1) & hash ↓ 检查桶是否为空? ├─ 是 → 直接插入新节点 └─ 否 → 遍历链表/红黑树 ├─ 存在相同 key → 更新 value └─ 不存在 → 插入新节点 ↓ 检查链表长度 ≥8 → 转红黑树(若数组长度 ≥64) ↓ 检查 size ≥ threshold → 扩容并迁移数据
4. 核心代码逻辑(简化版)
▼java复制代码public V put(K key, V value) { // 1. 计算哈希值 int hash = hash(key); // 2. 定位桶下标 Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // 初始化数组 i = (n - 1) & hash; p = tab[i]; // 3. 处理哈希冲突 if (p == null) { tab[i] = newNode(hash, key, value, null); // 空桶直接插入 } else { Node<K,V> e; K k; // 检查头节点是否匹配 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 { // 遍历链表 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 尾插法 if (binCount >= TREEIFY_THRESHOLD - 1) // 链表长度≥8 treeifyBin(tab, hash); // 可能转红黑树 break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 找到相同key p = e; } } // 更新value if (e != null) { V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; return oldValue; } } // 4. 扩容检查 if (++size > threshold) resize(); return null; }
5. 关键点总结
| 特性 | 说明 |
|---|---|
| 哈希扰动 | 高位与低位异或,减少哈希冲突概率。 |
| 尾插法 | JDK 1.8 后使用尾插法,避免多线程扩容时链表死循环(JDK 1.7 头插法存在此问题)。 |
| 链表转红黑树 | 链表长度 ≥8 且数组长度 ≥64 时转换,优化查询性能(O(n) → O(logn))。 |
| 扩容机制 | 数组长度翻倍,迁移数据时通过高位判断新位置。 |
| 线程不安全 | put 操作非原子性,多线程环境需使用 ConcurrentHashMap。 |
6. 应用场景与优化建议
- 高频写入场景:若预知数据量,初始化时指定容量(如
new HashMap<>(initialCapacity)),减少扩容次数。 - 键的设计:确保
key的hashCode()和equals()方法正确重写,避免哈希冲突。 - 性能权衡:链表转红黑树需要额外空间,适用于极端哈希冲突场景。
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
作者分享
使用spring boot 3.4.4 整合knife4j 4.4.0接口文档时,在访问接口文档时可能会出现错误java.lang.NoSuchMethodError:'void org.springframework.web.method.ControllerAdviceBean.<init>(java.lang.Object)',需在配置的全局异常类前添加@Hidden即可。详情见:https://springdoc.org/#Introduction
3
路漫漫其修远兮......
1
ArrayList和LinkedList的区别是什么?
3
什么是服务雪崩,怎么解决这个问题?
3
负载均衡是如何实现的?Ribbon负载均衡策略有哪些?如何自定义负载均衡策略?
1
