HashMap中put方法流程图

博客内容提及插入图片描述,但未给出更多关键信息。
<think>嗯,用户想了解HashMapput方法的原理。结合用户给出的引用内容,我需要整理一下这方面的知识体系。 用户主要在查询HashMapput方法的实现机制,尤其关注JDK1.8版本的特性。用户提供的引用材料涵盖了数据结构、哈希计算、冲突处理、树化机制等核心内容。 准备从数据结构开始说明,在JDK1.8中HashMap采用数组+链表/红黑树的复合结构。数组作为桶(bucket)存放链表头节点或树根节点,当链表长度超过8且数组容量达到64时会树化。这点在引用[3]中有提到树化转换条件。 哈希计算部分需要强调扰动函数的优化。JDK1.8的hash()方法将key的hashCode高16位与低16位异或,减少哈希碰撞。引用[2]提到使用hashCode()和哈希算法寻找索引,但没说明扰动细节,这点我会补充。 根据引用[3]描述的索引定位公式:(n-1)&hash,其中n是数组长度。这个位运算相当于取模但效率更高。当发生哈希冲突时,引用[2]说明会先判断节点类型:如果是树节点则调用红黑树的插入方法,否则遍历链表并考虑树化条件。 扩容机制方面着重说明引用[3]提到的优化:重新哈希时通过hash&oldCap判断元素位置。若结果为0则保持原索引,否则新位置=原索引+旧容量。这种设计避免重新计算哈希值,提升扩容效率。 最后需要强调线程安全问题。虽然不属于put原理本身,但实际应用很重要。考虑提醒用户HashMap非线程安全,多线程环境下应该用ConcurrentHashMap。 </think>好的,我们来详细解析一下 JDK 1.8 中 HashMap 的 `put(K key, V value)` 方法底层原理。整个过程围绕着**数组+链表/红黑树**的数据结构、**哈希计算**、**哈希冲突解决**以及**扩容**这几个核心概念展开: ### 核心数据结构与流程概述 1. **初始化与底层结构:** * HashMap 内部维护一个 `Node<K, V>[] table` 数组(通常称为桶数组或 bucket array)。`Node` 是存储键值对的基本单元,是一个链表节点(包含 `key`, `value`, `hash`, `next` 指针)。 * 在 JDK 1.8 中,当单个桶(数组元素)中的链表长度过长(达到阈值 8)**且**整个 `table` 数组的长度达到一定阈值(默认 64)时,该桶处的链表会转换为**红黑树(TreeNode)**,以提高极端情况下的查询效率($O(n)$ -> $O(\log n)$)[^1][^2]。 2. **put 方法的核心步骤:** * **Step 1: 计算 Key 的哈希值 (`hash`)** * 调用 `key.hashCode()` 获取原始哈希码。 * 关键优化:HashMap 内部使用 `(h = key.hashCode()) ^ (h >>> 16)` 进行一次**扰动计算**。 * 目的:**降低哈希碰撞**。原始哈希码的高 16 位与低 16 位进行异或,使得高位的变化也能影响最终的低位结果。因为后续确定桶位置时(`(n - 1) & hash`),当 table 长度 `n` 较小时(如初始 16),实际上是只用到了 `hash` 的低几位(如 `n-1=15`,二进制 `1111`,只用了低 4 位)。扰动让高位也参与到影响低位中,让哈希分布更均匀[^1][^2]。 * **Step 2: 计算桶索引 (`index`)** * 使用公式: `index = (table.length - 1) & hash` * 解释: * `table.length - 1`:因为桶数组长度 `n` 总是 2 的幂(如 16, 32, 64...),所以 `n-1` 的二进制表示是一个低位全为 `1` 的数(如 15 -> `1111`, 31 -> `11111`)。 * `& hash`:执行按位与操作 (`&`)。这相当于对 `hash` 值进行 `取模(n)` 操作 (`hash % n`),但效率远高于取模运算。 * 结果:`index` 的值在 `[0, table.length - 1]` 之间,确定了键值对应该放入哪个桶(数组哪个位置)[^1][^2][^3]。 * **Step 3: 处理目标桶 (`table[index]`)** * **情况 A:桶为空 (`table[index] == null`)** * 最简单的情况:直接在该索引位置创建一个新的 `Node` 节点存放键值对即可。 * **情况 B:桶不为空 (哈希碰撞发生)** * 需要遍历该桶位置的链表或红黑树,进行更新或插入: 1. **检查第一个节点:** * 比较 `table[index]` 节点的 `hash` 值和传入的 `hash` 值。 * 如果 `hash` 相等,再用 `equals()` 方法(或 `==`)比较 `key`。 * 如果 `key` 也相等:找到了**重复的 key**!用新 `value` **覆盖**旧的 `value`,返回旧 `value`。 2. **后续节点处理:** * 如果第一个节点不匹配,判断该桶结构是链表还是红黑树: * **如果是红黑树 (`TreeNode`):** 调用红黑树的 `putTreeVal()` 方法进行查找/插入。该方法会按照红黑树的规则查找 key。如果找到相同 key 则覆盖 value,找不到则插入新节点并在必要时调整树结构以保持平衡。 * **如果是链表:** 遍历链表(`next` 指针): * 对每个节点,重复步骤 1 的比较(`hash` 和 `key.equals()`)。 * 如果找到相同 key:覆盖 value,返回旧 value。 * 如果遍历到链表尾部 (`next == null`) 仍未找到:在链表尾部插入一个新的 `Node` 节点。 * **链表树化检查:** 插入新节点后,检查当前链表长度: * 如果插入后链表长度 **>= 8** (`TREEIFY_THRESHOLD = 8`),则调用 `treeifyBin(tab, hash)` 尝试将该链表转换为红黑树。 * 在 `treeifyBin` 方法中,会**先检查当前 `table` 的长度**: * 如果 `table` 长度 **< 64** (`MIN_TREEIFY_CAPACITY = 64`),则不进行树化,而是优先选择**扩容 (`resize()`)**,因为扩容本身就能分散节点,可能解决链表过长问题。 * 如果 `table` 长度 **>= 64**,则真正将该桶处的链表节点转换为 `TreeNode` 节点,并构建成一颗红黑树[^1][^2]。 * **Step 4: 修改计数与扩容检查** * 每次成功添加新节点(而非覆盖旧值)后,`size`(HashMap 中键值对总数)会加 1。 * 检查 `size` 是否超过了 `threshold`(阈值 = `capacity * loadFactor`,`capacity` 是 `table.length`,`loadFactor` 是负载因子,默认 0.75)。 * 如果 `size > threshold`:触发 **`resize()`** 扩容操作。 3. **扩容 (`resize()`) 原理:** * **目的:** 增大桶数组容量,减少哈希碰撞,为链表树化或保持性能提供空间。 * **过程:** 1. 创建一个新的 `Node` 数组,其容量是原数组容量的 **2 倍**(`newCap = oldCap << 1`)。 2. 遍历原数组的每一个桶(链表或树)。 3. 将原桶中的每个节点重新计算其在新数组中的位置(`(newCap - 1) & hash`),并将其移动到新数组对应的桶中。 * **JDK 1.8 优化:** 重新计算索引 (`index`) 时,利用了一个特性: * 因为 `newCap = oldCap * 2`(是 2 的幂),所以 `newCap - 1` 的二进制表示比 `oldCap - 1` 多一个高位 `1`(例如,16 -> 15(`1111`), 扩容到 32 -> 31(`11111`))。 * 节点在新数组中的位置只可能是两个值之一:**原位置 `oldIndex`** 或 **原位置 + 原容量 `oldIndex + oldCap`**。 * **判断依据:** 计算 `(e.hash & oldCap)`(注意这里是 `oldCap`,不是 `oldCap-1`): * 结果为 **0**:节点在新数组中的位置 = **原位置 (`oldIndex`)**。 * 结果**不为 0**:节点在新数组中的位置 = **原位置 + 原容量 (`oldIndex + oldCap`)**。 * **优点:** 省去了重新计算 `hash` 值的开销(`hash` 值本身不变),只需要判断一次位运算 (`hash & oldCap`) 的结果,就可以高效地将节点分配到新数组的两个桶中(要么在原桶,要么在原桶+oldCap 位置)。这个优化显著提升了扩容效率[^3]。 * 树节点在移动过程中,如果树变得太小(节点数 <= `UNTREEIFY_THRESHOLD = 6`),会被转换回链表。 ### 总结流程图 ```mermaid graph TD A[调用 putkey, value] --> B[计算 key 的 hash = key.hashCode ^ hashCode>>>16] B --> C[计算桶索引 index = n-1 & hash] C --> D{桶 table[index]为空?} D -- Yes --> E[创建新 Node 插入] D -- No --> F{桶中第一个节点 key 匹配? hash 和 equals} F -- Yes --> G[覆盖 value] F -- No --> H{桶结构是红黑树?} H -- Yes --> I[调用 putTreeVal 插入/覆盖] I -- 新节点插入? --> J[增加 size] H -- No 链表 --> K[遍历链表] K --> L{找到相同 key?} L -- Yes --> M[覆盖 value] L -- No 到尾节点 --> N[尾部插入新 Node] N --> O[增加 size] O --> P{链表长度 >= 8?} P -- Yes --> Q{table 长度 >= 64?} Q -- Yes --> R[转换链表为红黑树] Q -- No --> S[优先触发 resize 扩容] P -- No --> T[继续后续流程] E & J & R --> U[增加 size] U --> V{size > threshold?} V -- Yes --> W[触发 resize 扩容] V -- No --> X[流程结束] G & M -- 覆盖操作 --> X ``` ### 关键点回顾 1. **扰动哈希:** `(h = key.hashCode()) ^ (h >>> 16)` 提升散列均匀性。 2. **索引计算:** `index = (table.length - 1) & hash` 高效定位桶。 3. **冲突解决:** 通过链表或红黑树解决同一个桶内的哈希冲突。 4. **树化条件:** 链表长度 >= 8 **且** 数组长度 >= 64 时才树化,否则优先扩容。 5. **扩容触发:** `size > threshold` (capacity * loadFactor)。 6. **扩容优化:** 利用 `(e.hash & oldCap) == 0` 高效定位节点在新数组的位置(原位置或原位置+oldCap),避免重新计算hash。 7. **覆盖规则:** Key 相同(`hash` 相等且 `key.equals()` 为 true)时覆盖旧值。 通过以上步骤和优化,HashMap 的 `put` 方法能够在平均情况下实现高效的 $O(1)$ 时间复杂度(常数时间)的插入操作,并在冲突较多时通过链表或红黑树维持可接受的性能。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值