豪通驾校 發表於 2025-7-7 08:15:00

HashMap集合--基本操作流程的源码可视化

<blockquote>
<p>本文主要包含:HashMap 插入过程、扩容过程、查询过程和删除过程的源码可视化</p>
<p>文章对应的视频连接:https://www.bilibili.com/video/BV1wM3KzaE3d/</p>
</blockquote>
<h2 id="1-操作流程">1. 操作流程</h2>
<h3 id="11-插入过程putk-key-v-value">1.1. 插入过程(<code>put(K key, V value)</code>)</h3>
<p>插入流程主要涉及四种操作:扩容(首层扩容和阈值扩容)、单节点插入(无哈希冲突的情况)、链表遍历插入(冲突节点不超8个的情况)、红黑树插入。</p>
<p>插入节点的全流程图:</p>
<p><img src="https://img2024.cnblogs.com/blog/1209017/202507/1209017-20250707080527986-463338393.jpg" alt="image" loading="lazy"></p>
<h3 id="12-扩容过程resize">1.2. 扩容过程(<code>resize()</code>)</h3>
<p>扩容条件、扩容涉及到的链表挂载、链表树化、树转链表等等,都在前一篇四次扩容的文章中讲到,渐进式的学习HashMap扩容。</p>
<p><strong>HashMap扩容源码可视化</strong><br>
文章链接:https://mp.weixin.qq.com/s/J3kU51hb-GcM4Rsp7QCIFw<br>
视频链接:https://www.bilibili.com/video/BV1wM3KzaE3d/</p>
<h3 id="13-查询过程getobject-key">1.3. 查询过程(<code>get(Object key)</code>)</h3>
<ol>
<li>
<p><strong>空表或 <code>key == null</code></strong> :立即返回 <code>null</code>(<code>null</code> key 存储在索引 0)。</p>
</li>
<li>
<p><strong>计算 <code>hash</code>、下标 <code>i</code></strong></p>
</li>
<li>
<p><strong>遍历</strong></p>
<ul>
<li>
<p>如果 <code>table</code> 为单链表,逐节点比较 <code>hash</code> 与 <code>key.equals</code>;</p>
</li>
<li>
<p>如果为红黑树,调用 <code>TreeNode</code> 的 <code>getTreeNode</code>,按树结构快速查找(对数复杂度)。</p>
</li>
</ul>
</li>
</ol>
<p>查找元素全流程图:</p>
<p><img src="https://img2024.cnblogs.com/blog/1209017/202507/1209017-20250707080541122-1707330028.jpg" alt="image" loading="lazy"></p>
<h3 id="14-删除过程removeobject-key">1.4. 删除过程(<code>remove(Object key)</code>)</h3>
<p>最后将展示单链表的节点删除和红黑树节点的删除可视化过程。</p>
<ol>
<li>
<p>计算 <code>hash</code>、下标 <code>i</code>。</p>
</li>
<li>
<p>定位到槽位的链表或树,找到目标节点。</p>
</li>
<li>
<p>单链表:直接断链跳过;红黑树:调用 <code>removeTreeNode</code> 完成删除。</p>
</li>
<li>
<p><code>size--</code>,更新 <code>modCount</code>,返回被删节点的值。</p>
</li>
</ol>
<h4 id="删除链表节点">删除链表节点</h4>
<p>跟普通链表删除节点一样简单,下面直接通过动图来理解</p>
<p><img src="https://img2024.cnblogs.com/blog/1209017/202507/1209017-20250707080557119-1019341158.gif" alt="image" loading="lazy"></p>
<h4 id="删除红黑树节点">删除红黑树节点</h4>
<p>对于链表的删除处理是很简单很好理解,但是对于红黑树的删除就会比较复杂。在HashMap中,红黑树节点删除的可视化:<br>
<img src="https://img2024.cnblogs.com/blog/1209017/202507/1209017-20250707080604575-1682072513.gif" alt="image" loading="lazy"></p>
<p><img src="uploading..." alt="image" loading="lazy"></p>
<p><strong>关键步骤大致分为四步</strong>:寻找替换节点、进入待删除状态、红黑树平衡调整和最终删除节点。</p>
<p>最主要的两步源码如下,其次就是红黑树数据结构删除过程的理解:</p>
<p><strong>寻找替换</strong>:寻找中序后继节点作为替换节点。比如:红黑树左中右序为1、2、3、4,删除了2节点,那么就找3节点作为替换节点;如果删除3节点,那么就找4节点作为替换。对应的源代码如下</p>
<pre><code class="language-java">if (pl != null &amp;&amp; pr != null) {
        // 如果左右都不为空,找中序的后继节点替换,(右子树最靠左的节点)
        TreeNode&lt;K,V&gt; s = pr, sl;
        while ((sl = s.left) != null)
                s = sl;
               
        ...
}
</code></pre>
<p><strong>删除黑节点树平衡</strong>:</p>
<p>删除的主要源码如下,已为每一行源码附上注释。</p>
<pre><code class="language-java">// map: 当前所在的 HashMap 实例
// tab: 哈希表数组(即 table)
// movable=true
final void removeTreeNode(HashMap&lt;K,V&gt; map, Node&lt;K,V&gt;[] tab,
                        boolean movable) {
    int n;
    // 如果表为 null 或长度为 0,直接返回(无操作)
    if (tab == null || (n = tab.length) == 0)
      return;
    // 用哈希值定位当前节点所在的桶 index
    int index = (n - 1) &amp; hash;
    // 获取根节点
    TreeNode&lt;K,V&gt; first = (TreeNode&lt;K,V&gt;)tab, root = first, rl;
    // 从链表中断开当前节点(this),红黑树同时是双向链表这点必须知道,所以才有维护链表的操作
    TreeNode&lt;K,V&gt; succ = (TreeNode&lt;K,V&gt;)next, pred = prev;
    if (pred == null)
      tab = first = succ;
    else
      pred.next = succ;
    if (succ != null)
      succ.prev = pred;
    // 如果断链后没有剩余节点(即只有当前一个节点),直接返回
    if (first == null)
      return;

    if (root.parent != null)
      root = root.root();
    if (root == null
      || (movable
            &amp;&amp; (root.right == null
                || (rl = root.left) == null
                || rl.left == null))) {
      tab = first.untreeify(map);// too small
      return;
    }
    // 红黑树删除操作
    TreeNode&lt;K,V&gt; p = this, pl = left, pr = right, replacement;
    //处理删除节点p 同时有左右子节点的情况
    if (pl != null &amp;&amp; pr != null) {

      // 如果左右都不为空,找中序的后继替换,(右子树最靠左的节点)
      TreeNode&lt;K,V&gt; s = pr, sl;
      while ((sl = s.left) != null) // find successor
            s = sl;

      // 节点颜色交换
      boolean c = s.red; s.red = p.red; p.red = c; // swap colors

      // 交换结构:把后继节点换上来
      TreeNode&lt;K,V&gt; sr = s.right;
      TreeNode&lt;K,V&gt; pp = p.parent;
      // p 是 s 的直接父节点
      if (s == pr) {
            p.parent = s;
            s.right = p;
      }
      else {
            TreeNode&lt;K,V&gt; sp = s.parent;
            if ((p.parent = sp) != null) {
                if (s == sp.left)
                  sp.left = p;
                else
                  sp.right = p;
            }
            if ((s.right = pr) != null)
                pr.parent = s;
      }
      // 调整左子树与父指针:p 的左右清空(它即将被删),s 左右指针都设置好(接替 p)
      p.left = null;
      if ((p.right = sr) != null)
            sr.parent = p;
      if ((s.left = pl) != null)
            pl.parent = s;

      // s 接替 p 成为新的 root 的子节点
      if ((s.parent = pp) == null)
            root = s;
      else if (p == pp.left)
            pp.left = s;
      else
            pp.right = s;

      // 设置替换节点
      if (sr != null)
            replacement = sr;
      else
            replacement = p;
    }

    //处理删除节点p 有一个或没有子节点情况
    else if (pl != null)
      replacement = pl;
    else if (pr != null)
      replacement = pr;
    else
      replacement = p;

    // 让 replacement 替换掉 删除节点p 的位置
    if (replacement != p) {
      TreeNode&lt;K,V&gt; pp = replacement.parent = p.parent;
      if (pp == null)
            root = replacement;
      else if (p == pp.left)
            pp.left = replacement;
      else
            pp.right = replacement;
      p.left = p.right = p.parent = null;
    }

    // 如果删除节点p 是黑节点,需要平衡红黑树
    TreeNode&lt;K,V&gt; r = p.red ? root : balanceDeletion(root, replacement);

    // 如果 p 等于 replacement,说明删除的节点是叶子节点,断开叶子节点
    if (replacement == p) {
      TreeNode&lt;K,V&gt; pp = p.parent;
      p.parent = null;
      if (pp != null) {
            if (p == pp.left)
                pp.left = null;
            else if (p == pp.right)
                pp.right = null;
      }
    }

    // 将新的 root 移动到链表最前(优化访问)
    if (movable)
      moveRootToFront(tab, r);
}
</code></pre>
<h2 id="2-性能与并发考虑">2. 性能与并发考虑</h2>
<p><strong>时间复杂度</strong></p>
<table>
<thead>
<tr>
<th>操作</th>
<th>平均时间复杂度</th>
<th>最坏时间复杂度</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>get</code></td>
<td>O(1)</td>
<td>O(log n)</td>
<td>红黑树查找最坏 O(log n)</td>
</tr>
<tr>
<td><code>put</code></td>
<td>O(1)</td>
<td>O(log n)</td>
<td>链表树化后插树最坏 O(log n)</td>
</tr>
<tr>
<td><code>remove</code></td>
<td>O(1)</td>
<td>O(log n)</td>
<td>红黑树删除维护平衡 O(log n)</td>
</tr>
<tr>
<td><code>resize</code></td>
<td>O(1)</td>
<td>O(n)</td>
<td>摊销成本后每次插入 O(1)</td>
</tr>
<tr>
<td>遍历全部元素</td>
<td>O(n)</td>
<td>O(n)</td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>并发风险</strong></p>
<p><code>HashMap</code> 非线程安全,在多线程无外部同步时可能出现数据丢失或死循环(扩容时环路)。</p>
<p>多线程并发场景推荐使用 <code>ConcurrentHashMap</code>,或者对 HashMap 外层加锁(如 <code>Collections.synchronizedMap</code>,串行效率低)</p>
<h2 id="3-在-hashmap-中红黑树同时是双向链表">3. 在 HashMap 中红黑树同时是双向链表?</h2>
<p><strong>链表节点(未树化)</strong>:<code>Node&lt;K,V&gt;</code> 类型,只包含:</p>
<pre><code class="language-java">Node&lt;K,V&gt; {
    final int hash;
    final K key;
    V value;
    Node&lt;K,V&gt; next;
}
</code></pre>
<p>✅ 是 <strong>单向链表</strong>。</p>
<p><strong>树化节点(TreeNode)</strong>:扩展自 <code>Node&lt;K,V&gt;</code>,添加了:</p>
<pre><code class="language-java">TreeNode&lt;K,V&gt; extends Node&lt;K,V&gt; {
    TreeNode&lt;K,V&gt; parent;
    TreeNode&lt;K,V&gt; left;
    TreeNode&lt;K,V&gt; right;
    TreeNode&lt;K,V&gt; prev; // 🔥 这是额外的双向链表字段
    boolean red;
}
</code></pre>
<p>✅ 是 <strong>双向链表 + 红黑树结构</strong>。</p>
<h3 id="31-红黑树根节点始终是双向链表的头节点">3.1. 红黑树根节点始终是双向链表的头节点</h3>
<p>这里所说的双向链表结构指的是:<strong>在同一个桶中</strong>的红黑树。</p>
<p>在不同的桶中,红黑树之间是没有联系的,也不存在双向链表。</p>
<p><code>moveRootToFront</code> 这个方法有两个作用</p>
<ul>
<li>
<p>更新数组桶指向新的根节点</p>
</li>
<li>
<p>更新根节点为双向链表的头节点,并将旧的根节点作为下一节点接上(就是新的根节点和旧的根节点互换位置)</p>
</li>
</ul>
<pre><code class="language-java">static &lt;K,V&gt; void moveRootToFront(Node&lt;K,V&gt;[] tab, TreeNode&lt;K,V&gt; root) {
    int n;
    if (root != null &amp;&amp; tab != null &amp;&amp; (n = tab.length) &gt; 0) {
      int index = (n - 1) &amp; root.hash;
      // 获取旧的根节点
      TreeNode&lt;K,V&gt; first = (TreeNode&lt;K,V&gt;)tab;
      if (root != first) {
            Node&lt;K,V&gt; rn;
            // 数组桶指向新的根节点
            tab = root;

            // 断开 root 在原链表中的连接:先取出root上一节点
            TreeNode&lt;K,V&gt; rp = root.prev;
            // 再将前后节点连接起来,从而断开root 在原链表中的连接
            if ((rn = root.next) != null)
                ((TreeNode&lt;K,V&gt;)rn).prev = rp;
            if (rp != null)
                rp.next = rn;

            // 新的根节点调整为头节点
            if (first != null)
                first.prev = root;
            // 旧的根节点成为头节点的下一节点
            root.next = first;
            root.prev = null;
      }
      assert checkInvariants(root);
    }
}
</code></pre>
<p>总的来说,没什么深奥的,就是单链表和双链表的作用区别,为了任意树节点都可以更快的找到上一节点,提高操作效率。</p>
<h2 id="4-总结">4. 总结</h2>
<p>HashMap插入流程、扩容流程、查询流程,以及删除节点时链表和红黑树的处理。对 HashMap 会有一个基本而完整的理解。接下来可以深入学习红黑树数据结构,这是学习HashMap、LinkedHashMap、TreeMap等集合必须掌握的数据结构。</p>
<p><img src="https://img2024.cnblogs.com/blog/1209017/202507/1209017-20250707080510525-540449035.gif" alt="image" loading="lazy"></p>
<p>Java集合--HashMap底层原理可视化,秒懂扩容、链化、树化</p>
<p>Java集合--从本质出发理解HashMap</p>
<p>Java集合--LinkedList源码可视化</p>
<p>Java集合源码--ArrayList的可视化操作过程</p>
<p>掌握设计模式的两个秘籍</p>
<p>查看往期设计模式文章的:设计模式</p>
<p>超实用的SpringAOP实战之日志记录</p>
<p>2023年下半年软考考试重磅消息</p>
<p>通过软考后却领取不到实体证书?</p>
<p>计算机算法设计与分析(第5版)</p>
<p>Java全栈学习路线、学习资源和面试题一条龙</p>
<p>软考证书=职称证书?</p>
<p>软考中级--软件设计师毫无保留的备考分享</p>
<p>原创不易,觉得还不错的,三连支持:点赞、分享、推荐↓</p><br><br>
来源:https://www.cnblogs.com/dennyLee2025/p/18969786
頁: [1]
查看完整版本: HashMap集合--基本操作流程的源码可视化