重庆鲜面店刘永菊 發表於 2026-4-21 18:44:00

HashMap 源码深度解析

<h2 id="引言">引言</h2>
<p>为什么 HashMap 是 Java 中最常用、最重要的数据结构?</p>
<p>核心原因就一个:<strong>性能</strong>。常见的基础数据结构中,数组查询快但插入删除慢,链表插入快但查询慢。HashMap 综合了数组和链表的优点,将查询与插入的效率都控制在近似 O(1) 的复杂度内。</p>
<p>但 HashMap 的设计远不止于此。容量为什么是 2 的幂?哈希扰动函数 <code>spread()</code> 如何防止哈希冲突?链表转红黑树的阈值为什么选 8?本文将从源码级别逐一揭示这些设计背后的原理。</p>
<p>学完本文,你将掌握:</p>
<ol>
<li>HashMap 的底层实现原理(数组 + 链表 + 红黑树)</li>
<li>put 方法的完整执行流程与哈希扰动算法</li>
<li>扩容机制与 2 倍扩容的根本原因</li>
<li>HashMap 为什么线程不安全?并发下会产生什么事故?</li>
<li>HashMap 的容量为什么必须是 2 的幂?</li>
<li>HashMap 在 Java 8 版本中做了哪些变更?</li>
</ol>
<h2 id="简介">简介</h2>
<p>HashMap 的底层数据结构由数组、链表和红黑树组成,核心是基于数组实现的。为了解决哈希冲突,采用拉链法,于是引入了链表结构。为了解决链表过长导致的查询性能下降,Java 8 引入了红黑树结构。</p>
<p>HashMap 类中有两个关键的阈值常量:</p>
<ul>
<li><code>TREEIFY_THRESHOLD = 8</code>:当链表长度达到 8 时,链表会转换为红黑树</li>
<li><code>UNTREEIFY_THRESHOLD = 6</code>:当红黑树节点数减少到 6 时,红黑树会退化为链表</li>
<li><code>MIN_TREEIFY_CAPACITY = 64</code>:只有数组容量达到 64 时才会触发树化,否则优先扩容数组而不是树化</li>
</ul>
<p>这三个常量配合使用,目的是避免频繁的树化和退化操作。如果链表只是短暂变长,不会触发树化;如果红黑树只是短暂变小,不会立即退化。</p>
<blockquote>
<p><strong>💡 核心提示</strong>:为什么树化阈值是 8、退化阈值是 6?这背后有统计学依据。HashMap 中节点的分布服从泊松分布(Poisson Distribution)。在理想的 hash 散列下,链表长度达到 8 的概率约为 0.00000006(千万分之六),几乎不可能自然发生。如果达到了 8,说明 hash 冲突严重,此时用红黑树代替链表可以将 O(n) 退化为 O(log n)。退化为 6 是为了留出缓冲区间(8→6),避免在临界值附近频繁树化/退化,这就是所谓的"迟滞效应"(Hysteresis)。</p>
</blockquote>
<h3 id="hashmap-类架构图">HashMap 类架构图</h3>
<div class="mermaid">classDiagram
    class AbstractMap {
      &lt;&lt;abstract&gt;&gt;
      +containsKey(Object) boolean
      +containsValue(Object) boolean
      +isEmpty() boolean
      +size() int
      +clear() void
    }

    class Map {
      &lt;&lt;interface&gt;&gt;
      +put(K, V) V
      +get(Object) V
      +remove(Object) V
      +containsKey(Object) boolean
      +containsValue(Object) boolean
      +entrySet() Set~Entry~
      +keySet() Set~K~
      +values() Collection~V~
    }

    class HashMap {
      -transient Node[] table
      -transient int size
      -transient int threshold
      -transient int modCount
      -final float loadFactor
      +put(K, V) V
      +get(Object) V
      +remove(Object) V
      +resize() Node[]
      -hash(Object) int
      -putVal(int, K, V, boolean, boolean) V
      -getNode(int, Object) Node
      -treeifyBin(Node[], int) void
      -tableSizeFor(int) int
    }

    class LinkedHashMap {
      -transient Entry head
      -transient Entry tail
      -final boolean accessOrder
    }

    class Node {
      +final int hash
      +final K key
      +V value
      +Node next
      +getKey() K
      +getValue() V
      +setValue(V) V
    }

    class TreeNode {
      +TreeNode parent
      +TreeNode left
      +TreeNode right
      +TreeNode prev
      +boolean red
      +putTreeVal(HashMap, Node[], int, K, V) TreeNode
      +getTreeNode(int, Object) TreeNode
      +removeTreeNode(HashMap, Node[], boolean) void
      +split(HashMap, Node[], int, int) void
    }

    class Cloneable {
      &lt;&lt;interface&gt;&gt;
    }

    class Serializable {
      &lt;&lt;interface&gt;&gt;
    }

    Map &lt;|.. AbstractMap
    AbstractMap &lt;|-- HashMap
    HashMap &lt;|-- LinkedHashMap
    HashMap o-- Node : contains
    Node &lt;|-- TreeNode
    HashMap ..&gt; Cloneable : implements
    HashMap ..&gt; Serializable : implements
</div><p>HashMap 的核心工作原理可以用下面的流程图概括:</p>
<div class="mermaid">flowchart TD
    Start["初始化: table=null, size=0, threshold=0"] --&gt; Put["put(key, value)"]
    Put --&gt; Hash["hash(key): h = key.hashCode()\n返回 h ^ (h &gt;&gt;&gt; 16)"]
    Hash --&gt; CheckTable{"table 为空?"}
    CheckTable --&gt;|是| Resize["resize(): 初始化数组\n容量=16 或 tableSizeFor(指定容量)"]
    CheckTable --&gt;|否| CalcIndex["计算下标: i = (n - 1) &amp; hash"]
    Resize --&gt; CalcIndex
    CalcIndex --&gt; CheckSlot{"tab 是否为空?"}
    CheckSlot --&gt;|是| Insert["直接插入新节点"]
    CheckSlot --&gt;|否| CheckKey{"key 是否已存在?"}
    CheckKey --&gt;|是| UpdateValue["覆盖旧值, 返回旧值"]
    CheckKey --&gt;|否| CheckType{"节点类型?"}
    CheckType --&gt;|红黑树| TreeInsert["putTreeVal: 红黑树插入"]
    CheckType --&gt;|链表| ListInsert["遍历链表, 尾插法追加"]
    ListInsert --&gt; CheckLen{"链表长度 &gt;= 8?"}
    CheckLen --&gt;|是| CheckCap{"数组容量 &gt;= 64?"}
    CheckCap --&gt;|是| Treeify["treeifyBin: 链表转红黑树"]
    CheckCap --&gt;|否| ArrayResize["resize(): 扩容数组"]
    CheckLen --&gt;|否| CheckExpand
    Treeify --&gt; CheckExpand{"size &gt; threshold?"}
    ArrayResize --&gt; CheckExpand
    Insert --&gt; CheckExpand
    CheckExpand --&gt;|是| Resize2["resize(): 容量翻倍\n链表拆分为低位/高位两条链表"]
    CheckExpand --&gt;|否| Done["modCount++, size++, 返回null"]
    Resize2 --&gt; Done
    Get["get(key)"] --&gt; GetHash["hash(key) 计算哈希值"]
    GetHash --&gt; GetIndex["计算下标: (n-1) &amp; hash"]
    GetIndex --&gt; GetCheck{"找到 key?"}
    GetCheck --&gt;|是| ReturnVal["返回 value"]
    GetCheck --&gt;|否| ReturnNull["返回 null"]
</div><h2 id="类属性">类属性</h2>
<p>再看一下 HashMap 类中有哪些关键属性:</p>
<pre><code class="language-java">public class HashMap&lt;K, V&gt; extends AbstractMap&lt;K, V&gt;
      implements Map&lt;K, V&gt;, Cloneable, Serializable {

    /**
   * 默认容量大小,1 &lt;&lt; 4 = 16
   */
    static final int DEFAULT_INITIAL_CAPACITY = 1 &lt;&lt; 4;

    /**
   * 负载系数,容量超过 threshold = capacity * loadFactor 时触发扩容
   * 默认 16 * 0.75 = 12 个元素时扩容
   */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
   * 容量最大值,2 的 30 次方
   */
    static final int MAXIMUM_CAPACITY = 1 &lt;&lt; 30;

    /**
   * 链表转红黑树的阈值:链表长度 &gt; 8 时树化
   */
    static final int TREEIFY_THRESHOLD = 8;

    /**
   * 红黑树退化为链表的阈值:节点数 &lt; 6 时退化
   */
    static final int UNTREEIFY_THRESHOLD = 6;

    /**
   * 树化的最小数组容量:只有 table 容量 &gt;= 64 才会树化
   */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
   * 存储元素的数组,容量始终为 2 的幂
   */
    transient Node&lt;K, V&gt;[] table;

    /**
   * 实际存储的键值对数量
   */
    transient int size;

    /**
   * 扩容阈值,当 size &gt; threshold 时触发扩容
   */
    transient int threshold;

    /**
   * 结构修改次数,用于 fail-fast 机制
   */
    transient int modCount;
}
</code></pre>
<p>其中 <code>Node</code> 是链表的节点:</p>
<pre><code class="language-java">static class Node&lt;K, V&gt; implements Map.Entry&lt;K, V&gt; {
    final int hash;    // 哈希值(缓存,避免重复计算)
    final K key;       // 键
    V value;         // 值
    Node&lt;K, V&gt; next;   // 后继节点

    Node(int hash, K key, V value, Node&lt;K, V&gt; next) {
      this.hash = hash;
      this.key = key;
      this.value = value;
      this.next = next;
    }
}
</code></pre>
<p>以及红黑树的节点 <code>TreeNode</code>(继承自 <code>LinkedHashMap.Entry</code>,本质是 <code>Node</code> 的子类):</p>
<pre><code class="language-java">static final class TreeNode&lt;K, V&gt; extends LinkedHashMap.Entry&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;    // 前驱节点(继承自 LinkedHashMap.Entry)
    boolean red;            // 节点颜色

    TreeNode(int hash, K key, V val, Node&lt;K, V&gt; next) {
      super(hash, key, val, next);
    }
}
</code></pre>
<h2 id="初始化">初始化</h2>
<p>HashMap 常见的初始化方法有两个:</p>
<ol>
<li>无参初始化</li>
<li>有参初始化,指定容量大小</li>
</ol>
<pre><code class="language-java">/**
* 无参初始化
*/
Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;();
/**
* 有参初始化,指定容量大小
*/
Map&lt;Integer, Integer&gt; map = new HashMap&lt;&gt;(10);
</code></pre>
<p>再看一下构造方法的底层实现:</p>
<pre><code class="language-java">/**
* 无参初始化
*/
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
}

/**
* 有参初始化,指定容量大小
*/
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}

/**
* 有参初始化,指定容量大小和负载系数
*/
public HashMap(int initialCapacity, float loadFactor) {
    // 校验参数
    if (initialCapacity &lt; 0) {
      throw new IllegalArgumentException("Illegal initial capacity: " +
                initialCapacity);
    }
    if (initialCapacity &gt; MAXIMUM_CAPACITY) {
      initialCapacity = MAXIMUM_CAPACITY;
    }
    if (loadFactor &lt;= 0 || Float.isNaN(loadFactor)) {
      throw new IllegalArgumentException("Illegal load factor: " +
                loadFactor);
    }
    this.loadFactor = loadFactor;
    // 计算出合适的容量大小(2的幂)
    this.threshold = tableSizeFor(initialCapacity);
}
</code></pre>
<p>可以看出,无参构造方法只初始化了负载系数。指定容量大小的有参构造方法也只是初始化了负载系数和 <code>threshold</code>(扩容阈值),<strong>两个方法都没有初始化数组大小</strong>。</p>
<p><code>tableSizeFor</code> 方法会将传入的容量值转换为大于等于它的最小的 2 的幂。例如 <code>tableSizeFor(10)</code> 返回 16,<code>tableSizeFor(20)</code> 返回 32。这个方法实现非常精妙:</p>
<pre><code class="language-java">/**
* 返回大于等于 cap 的最小的 2 的幂
*/
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n &gt;&gt;&gt; 1;
    n |= n &gt;&gt;&gt; 2;
    n |= n &gt;&gt;&gt; 4;
    n |= n &gt;&gt;&gt; 8;
    n |= n &gt;&gt;&gt; 16;
    return (n &lt; 0) ? 1 : (n &gt;= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
</code></pre>
<blockquote>
<p><strong>💡 核心提示</strong>:<code>tableSizeFor</code> 方法为什么先 <code>cap - 1</code>?如果不减 1,当 cap 本身就是 2 的幂时(比如 16),结果会变成 32,导致浪费一半空间。减 1 保证了 "恰好等于 2 的幂" 时不会翻倍。而右移 + 或运算的组合,能将最高位 1 右侧的所有位都填充为 1,最后 +1 就是下一个 2 的幂。</p>
</blockquote>
<p>如果再有面试官问你,HashMap 初始化的时候数组大小是多少?答案是 <strong>0</strong>(或者说未初始化),因为 HashMap 采用了<strong>懒加载</strong>策略,数组在第一次 <code>put</code> 时才会通过 <code>resize()</code> 方法初始化。</p>
<h2 id="深入-hash-算法">深入 hash 算法</h2>
<p>在深入 put 方法之前,先看一下 HashMap 的 <code>hash()</code> 方法:</p>
<pre><code class="language-java">static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h &gt;&gt;&gt; 16);
}
</code></pre>
<p>这个方法做了两件事:</p>
<ol>
<li>如果 key 为 null,返回 0(HashMap 允许 null 作为 key)</li>
<li>否则,取 key 的 <code>hashCode()</code>,然后将其<strong>无符号右移 16 位</strong>,再与原值做<strong>异或运算</strong></li>
</ol>
<p>为什么要这样做?HashMap 计算数组下标用的是 <code>(n - 1) &amp; hash</code>,其中 n 是数组容量(2 的幂)。当 n 较小时(比如 16),<code>n - 1</code> 只有低 4 位是 1,高位都是 0,这意味着 <code>&amp;</code> 运算只会用到 hash 的低位。<strong>如果不同 key 的 hashCode 只有高位不同、低位相同,就会产生哈希冲突</strong>。通过右移 16 位再异或,把高位的信息混入低位,让高位也参与下标计算,从而<strong>减少哈希冲突</strong>。</p>
<blockquote>
<p><strong>💡 核心提示</strong>:为什么选择右移 16 位?因为 int 是 32 位,右移 16 位恰好将高 16 位与低 16 位对齐,异或后高低位的信息混合在一起。这是一个在性能和散列质量之间的权衡:更多次的混合(比如多次右移+异或)可以提高散列质量,但会降低性能;16 位一次的混合在实践中已足够好。</p>
</blockquote>
<h2 id="put-源码">put 源码</h2>
<p>put 方法的流程如下:</p>
<ol>
<li>计算 key 的 hash 值</li>
<li>如果数组为空,调用 <code>resize()</code> 初始化</li>
<li>根据 <code>(n - 1) &amp; hash</code> 计算数组下标</li>
<li>如果下标位置为空,直接插入</li>
<li>如果下标位置不为空,判断 key 是否已存在,存在则覆盖</li>
<li>如果是红黑树节点,执行红黑树插入</li>
<li>如果是链表节点,遍历链表尾插,长度达到 8 时树化</li>
<li>如果 <code>size &gt; threshold</code>,调用 <code>resize()</code> 扩容</li>
</ol>
<h3 id="put-方法详细流程图">put 方法详细流程图</h3>
<div class="mermaid">flowchart TD
    A["put(key, value)"] --&gt; B["hash(key) 计算扰动后的哈希值"]
    B --&gt; C{"table 是否为空?"}
    C --&gt;|是| D["resize() 初始化数组"]
    C --&gt;|否| E["i = (n-1) &amp; hash 计算下标"]
    D --&gt; E
    E --&gt; F{"tab == null?"}
    F --&gt;|是| G["tab = newNode() 直接插入"]
    F --&gt;|否| H{"p.hash == hash &amp;&amp; key匹配?"}
    H --&gt;|是| I["e = p 记录待覆盖节点"]
    H --&gt;|否| J{"p instanceof TreeNode?"}
    J --&gt;|是| K["putTreeVal() 红黑树插入"]
    J --&gt;|否| L["遍历链表尾插"]
    L --&gt; M{"binCount &gt;= 7?"}
    M --&gt;|是| N{"table.length &gt;= 64?"}
    N --&gt;|是| O["treeifyBin() 链表转红黑树"]
    N --&gt;|否| P["resize() 扩容数组"]
    M --&gt;|否| Q
    O --&gt; Q{"++size &gt; threshold?"}
    P --&gt; Q
    G --&gt; Q
    K --&gt; Q
    I --&gt; R["覆盖旧值, 返回旧值"]
    Q --&gt;|是| S["resize() 扩容"]
    Q --&gt;|否| T["modCount++, size++, 返回null"]
    S --&gt; T
    R --&gt; U["返回旧值"]
</div><h3 id="put-源码详解">put 源码详解</h3>
<p>再看一下 put 方法的具体源码实现:</p>
<pre><code class="language-java">/**
* put 方法入口
*/
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

/**
* 计算 hash 值(高位和低位都参与计算)
*/
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h &gt;&gt;&gt; 16);
}

/**
* 实际的put方法逻辑
* @param hash key对应的hash值
* @param key 键
* @param value 值
* @param onlyIfAbsent 如果为true,则只有当key不存在时才会put,否则会覆盖
* @param evict 如果为false,表处于创建模式
* @return 返回旧值
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node&lt;K, V&gt;[] tab;
    Node&lt;K, V&gt; p;
    int n, i;
    // 1. 如果数组为空,则执行初始化(resize 同时负责初始化和扩容)
    if ((tab = table) == null || (n = tab.length) == 0) {
      n = (tab = resize()).length;
    }
    // 2. 如果 key 对应下标位置元素不存在,直接插入即可
    if ((p = tab) == null) {
      tab = newNode(hash, key, value, null);
    } else {
      Node&lt;K, V&gt; e;
      K k;
      // 3. 如果头节点 key 匹配,直接结束,后续判断是否需要覆盖
      if (p.hash == hash &amp;&amp;
                ((k = p.key) == key || (key != null &amp;&amp; key.equals(k)))) {
            e = p;
      } else if (p instanceof TreeNode) {
            // 4. 判断下标位置的元素类型,如果是红黑树,则执行红黑树的插入逻辑
            e = ((TreeNode&lt;K, V&gt;) p).putTreeVal(this, tab, hash, key, value);
      } else {
            // 5. 否则执行链表的插入逻辑
            for (int binCount = 0; ; ++binCount) {
                // 6. 遍历链表,直到找到空位置为止
                if ((e = p.next) == null) {
                  // 7. 创建一个新的链表节点,并追加到末尾(尾插法)
                  p.next = newNode(hash, key, value, null);
                  // 8. 如果链表长度达到 8,则转换为红黑树
                  if (binCount &gt;= TREEIFY_THRESHOLD - 1) {
                        treeifyBin(tab, hash);
                  }
                  break;
                }
                // 9. 如果在链表中找到相同的 key,则结束
                if (e.hash == hash &amp;&amp;
                        ((k = e.key) == key || (key != null &amp;&amp; key.equals(k)))) {
                  break;
                }
                p = e;
            }
      }
      // 10. 判断是否需要覆盖旧值
      if (e != null) {
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null) {
                e.value = value;
            }
            afterNodeAccess(e);
            return oldValue;
      }
    }
    ++modCount;
    // 11. 判断是否需要扩容
    if (++size &gt; threshold) {
      resize();
    }
    afterNodeInsertion(evict);
    return null;
}
</code></pre>
<p>其中 <code>treeifyBin</code> 方法负责将链表转换为红黑树,但只有在数组容量达到 <code>MIN_TREEIFY_CAPACITY</code>(64)时才会真正树化,否则只是扩容数组:</p>
<pre><code class="language-java">final void treeifyBin(Node&lt;K, V&gt;[] tab, int hash) {
    int n, index;
    Node&lt;K, V&gt; e;
    // 如果数组容量 &lt; 64,优先扩容而不是树化
    if (tab == null || (n = tab.length) &lt; MIN_TREEIFY_CAPACITY) {
      resize();
    } else if ((e = tab) != null) {
      // 否则,将链表转换为红黑树
      TreeNode&lt;K, V&gt; hd = null, tl = null;
      // 遍历链表,将所有 Node 替换为 TreeNode
      do {
            TreeNode&lt;K, V&gt; p = new TreeNode&lt;&gt;(e.hash, e.key, e.value, null);
            if (tl == null) {
                hd = p;
            } else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
      } while ((e = e.next) != null);
      // 将 TreeNode 双向链表构造成红黑树
      if ((tab = hd) != null) {
            hd.treeify(tab);
      }
    }
}
</code></pre>
<blockquote>
<p><strong>💡 核心提示</strong>:Java 8 采用<strong>尾插法</strong>而非 Java 7 的头插法,这是为了解决多线程扩容时链表成环的问题。虽然 HashMap 本身不是线程安全的,但尾插法至少避免了在并发场景下出现死循环。真正的并发场景仍然应该使用 <code>ConcurrentHashMap</code>。</p>
</blockquote>
<h2 id="扩容源码">扩容源码</h2>
<p>再看一下扩容逻辑的具体实现:</p>
<h3 id="resize-详细流程图">resize 详细流程图</h3>
<div class="mermaid">flowchart TD
    A["resize() 入口"] --&gt; B["oldCap = table.length\noldThr = threshold"]
    B --&gt; C{"oldCap &gt; 0?"}
    C --&gt;|是| D{"oldCap &gt;= MAXIMUM_CAPACITY?"}
    D --&gt;|是| E["threshold = MAX_VALUE\n返回旧数组"]
    D --&gt;|否| F["newCap = oldCap &lt;&lt; 1\nnewThr = oldThr &lt;&lt; 1"]
    C --&gt;|否| G{"oldThr &gt; 0?"}
    G --&gt;|是| H["newCap = oldThr\n有参构造首次扩容"]
    G --&gt;|否| I["newCap = 16, newThr = 12\n无参构造首次扩容"]
    F --&gt; J{"newThr == 0?"}
    H --&gt; J
    I --&gt; J
    J --&gt;|是| K["newThr = newCap * loadFactor"]
    J --&gt;|否| L["threshold = newThr"]
    K --&gt; L
    L --&gt; M["newTab = new Node"]
    M --&gt; N{"oldTab != null?"}
    N --&gt;|否| O["返回 newTab"]
    N --&gt;|是| P["遍历旧数组 0 ~ oldCap"]
    P --&gt; Q{"节点数量?"}
    Q --&gt;|单节点| R["newTab = e"]
    Q --&gt;|红黑树| S["split() 拆分红黑树"]
    Q --&gt;|链表| T["hash &amp; oldCap == 0?\n拆分低位/高位链表"]
    T --&gt;|是| U["插入低位 newTab"]
    T --&gt;|否| V["插入高位 newTab"]
    R --&gt; W["继续遍历"]
    S --&gt; W
    U --&gt; W
    V --&gt; W
    W --&gt; P
    P --&gt; O
</div><h3 id="扩容源码详解">扩容源码详解</h3>
<pre><code class="language-java">/**
* 扩容(同时负责首次初始化)
*/
final Node&lt;K, V&gt;[] resize() {
    Node&lt;K, V&gt;[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 计算扩容后容量大小
    // 1. 如果原来容量大于0,说明不是第一次扩容,直接扩容为原来的2倍
    if (oldCap &gt; 0) {
      if (oldCap &gt;= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
      } else if ((newCap = oldCap &lt;&lt; 1) &lt; MAXIMUM_CAPACITY &amp;&amp;
                oldCap &gt;= DEFAULT_INITIAL_CAPACITY) {
            newThr = oldThr &lt;&lt; 1;
      }
    } else if (oldThr &gt; 0) {
      // 2. 把原来的阈值当成新的容量大小(有参构造首次put时走这个分支)
      newCap = oldThr;
    } else {
      // 3. 如果是第一次初始化(无参构造),则容量和阈值都用默认值
      newCap = DEFAULT_INITIAL_CAPACITY;
      newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 4. 如果新的阈值未设置,则重新计算扩容后阈值
    if (newThr == 0) {
      float ft = (float) newCap * loadFactor;
      newThr = (newCap &lt; MAXIMUM_CAPACITY &amp;&amp; ft &lt; (float) MAXIMUM_CAPACITY ?
                (int) ft : Integer.MAX_VALUE);
    }
    threshold = newThr;

    // 5. 创建一个新数组,容量使用上面计算的大小
    Node&lt;K, V&gt;[] newTab = (Node&lt;K, V&gt;[]) new Node;
    table = newTab;
    // 6. 遍历原来的数组,将元素插入到新数组
    if (oldTab != null) {
      for (int j = 0; j &lt; oldCap; ++j) {
            Node&lt;K, V&gt; e;
            if ((e = oldTab) != null) {
                oldTab = null;// 帮助 GC 回收
                // 7. 如果下标位置只有一个元素,则直接插入新数组即可
                if (e.next == null) {
                  newTab = e;
                } else if (e instanceof TreeNode) {
                  // 8. 如果下标位置元素类型是红黑树,则执行红黑树的拆分逻辑
                  ((TreeNode&lt;K, V&gt;) e).split(this, newTab, j, oldCap);
                } else {
                  // 9. 否则执行链表的拆分逻辑,使用 do-while 循环
                  // loHead、loTail表示低位链表的头尾节点
                  // hiHead、hiTail表示高位链表的头尾节点
                  Node&lt;K, V&gt; loHead = null, loTail = null;
                  Node&lt;K, V&gt; hiHead = null, hiTail = null;
                  Node&lt;K, V&gt; next;
                  do {
                        next = e.next;
                        // 10. 判断当前元素 hash &amp; oldCap 是否为0
                        //   为0 → 位置不变,插入低位链表
                        //   不为0 → 位置 = 原下标 + oldCap,插入高位链表
                        if ((e.hash &amp; 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);
                  // 11. 将低位链表插入到新数组中(位置不变)
                  if (loTail != null) {
                        loTail.next = null;
                        newTab = loHead;
                  }
                  // 12. 将高位链表插入到新数组中(位置 = j + oldCap)
                  if (hiTail != null) {
                        hiTail.next = null;
                        newTab = hiHead;
                  }
                }
            }
      }
    }
    return newTab;
}
</code></pre>
<p>重点关注第 9 步的链表拆分逻辑。这个方法非常巧妙:不是逐个计算元素在新数组中的下标,而是将原链表<strong>拆成两个链表,整体迁移</strong>。</p>
<p>假设原数组容量是 16,某个下标位置的链表元素分别是 <code>1 -&gt; 17 -&gt; 33 -&gt; 49</code>,这些元素的共同特点是对 16 求余等于 1。</p>
<p>扩容后新数组容量是 32,这些元素会拆分成两条链表:</p>
<ul>
<li>下标 1 的链表:<code>1 -&gt; 33</code>(<code>hash &amp; 16 = 0</code>,位置不变)</li>
<li>下标 17 的链表:<code>17 -&gt; 49</code>(<code>hash &amp; 16 = 16</code>,新位置 = 1 + 16 = 17)</li>
</ul>
<p>为什么 <code>hash &amp; oldCap</code> 能判断元素是否需要移动?因为容量是 2 的幂,扩容后多了一位。如果元素的 hash 值在这一位上是 0,下标不变;如果是 1,新下标 = 原下标 + oldCap。这样就不需要重新计算每个元素的下标,<strong>大幅提升了扩容效率</strong>。</p>
<blockquote>
<p><strong>💡 核心提示</strong>:负载因子为什么默认是 0.75?这是空间利用率和时间效率之间的最佳折中。根据泊松分布,负载因子为 0.75 时,链表长度达到 8 的概率极低(约千万分之六)。如果负载因子设为 1.0,虽然空间利用率高了,但哈希冲突会显著增加;如果设为 0.5,虽然冲突减少了,但空间浪费了一半。0.75 是经过大量实验验证的最优值。</p>
</blockquote>
<h2 id="哈希冲突解决从链表到红黑树">哈希冲突解决:从链表到红黑树</h2>
<p>当多个 key 的 hash 值计算出相同的数组下标时,就会发生哈希冲突。HashMap 采用<strong>拉链法</strong>解决冲突,将冲突的 key 串成链表挂在同一个桶位。但在极端情况下(恶意构造 hashCode 或大量冲突),链表会很长,查询退化为 O(n)。</p>
<p>Java 8 引入红黑树来解决这个问题。下面是链表到红黑树的转换流程图:</p>
<div class="mermaid">flowchart TD
    A["发生哈希冲突"] --&gt; B["计算桶位 i = (n-1) &amp; hash"]
    B --&gt; C{"tab 是否已有节点?"}
    C --&gt;|否| D["直接插入, 无冲突"]
    C --&gt;|是| E{"key 是否匹配?"}
    E --&gt;|是| F["覆盖旧值"]
    E --&gt;|否| G{"当前节点类型?"}
    G --&gt;|TreeNode| H["红黑树插入 putTreeVal"]
    G --&gt;|Node| I["链表尾插"]
    I --&gt; J{"链表长度 &gt;= 8?"}
    J --&gt;|否| K["继续等待"]
    J --&gt;|是| L{"数组容量 &gt;= 64?"}
    L --&gt;|否| M["resize() 扩容, 不树化"]
    L --&gt;|是| N["treeifyBin() 链表转红黑树"]
    N --&gt; O["每个 Node 转为 TreeNode"]
    O --&gt; P["treeify() 构建红黑树"]
    P --&gt; Q["红黑树就绪, 查询 O(log n)"]
    M --&gt; R{"扩容后该桶位链表长度 &lt; 6?"}
    R --&gt;|是| S["untreeify() 红黑树退化链表"]
    R --&gt;|否| Q
</div><blockquote>
<p><strong>💡 核心提示</strong>:红黑树的树化过程不是直接将链表中的节点逐个 insert 到空树中,而是先将所有节点转为 TreeNode 并用 prev/next 连成双向链表,再通过 <code>treeify()</code> 方法自底向上构建红黑树。这种方式比逐个插入更高效,且构建后会自动平衡。退化过程同理,<code>untreeify()</code> 会遍历树节点,逐个转回普通 Node 并重新连成单向链表。</p>
</blockquote>
<h2 id="get-源码">get 源码</h2>
<p>再看一下 get 方法源码实现:</p>
<pre><code class="language-java">/**
* get 方法入口
*/
public V get(Object key) {
    Node&lt;K, V&gt; e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

/**
* 查询节点方法
*/
final Node&lt;K, V&gt; getNode(int hash, Object key) {
    Node&lt;K, V&gt;[] tab;
    Node&lt;K, V&gt; first, e;
    int n;
    K k;
    // 1. 获取下标位置节点元素,命名为 first
    if ((tab = table) != null &amp;&amp; (n = tab.length) &gt; 0 &amp;&amp;
            (first = tab[(n - 1) &amp; hash]) != null) {
      // 2. 比较 first 节点哈希值与 key 值
      if (first.hash == hash &amp;&amp;
                ((k = first.key) == key || (key != null &amp;&amp; key.equals(k)))) {
            return first;
      }
      if ((e = first.next) != null) {
            // 3. 如果 first 节点类型是红黑树,就执行红黑树的查找逻辑
            if (first instanceof TreeNode) {
                return ((TreeNode&lt;K, V&gt;) first).getTreeNode(hash, key);
            }
            // 4. 否则,就执行链表的查找逻辑
            do {
                if (e.hash == hash &amp;&amp;
                        ((k = e.key) == key || (key != null &amp;&amp; key.equals(k)))) {
                  return e;
                }
            } while ((e = e.next) != null);
      }
    }
    // 5. 都没找到就返回 null
    return null;
}
</code></pre>
<p>get 方法的逻辑与 put 类似:先计算下标 → 检查头节点 → 根据节点类型选择查找方式。头尾节点的比较都优先用 <code>==</code> 判断引用是否相同,再用 <code>equals</code> 判断值是否相等,这是一种<strong>短路优化</strong>。</p>
<h2 id="remove-源码">remove 源码</h2>
<p>再看一下 remove 方法源码:</p>
<pre><code class="language-java">/**
* 删除方法入口
*/
public V remove(Object key) {
    Node&lt;K, V&gt; e;
    return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
}

/**
* 删除节点方法
*/
final Node&lt;K, V&gt; removeNode(int hash, Object key, Object value,
                            boolean matchValue, boolean movable) {
    Node&lt;K, V&gt;[] tab;
    Node&lt;K, V&gt; p;
    int n, index;
    // 1. 判断数组是否为空,下标位置节点是否为空
    if ((tab = table) != null &amp;&amp; (n = tab.length) &gt; 0 &amp;&amp;
            (p = tab) != null) {
      Node&lt;K, V&gt; node = null, e;
      K k;
      V v;
      // 2. 判断下标节点 key 是否与传入的 key 相等
      if (p.hash == hash &amp;&amp;
                ((k = p.key) == key || (key != null &amp;&amp; key.equals(k)))) {
            node = p;
      } else if ((e = p.next) != null) {
            // 3. 如果节点类型是红黑树,就执行红黑树的查找逻辑
            if (p instanceof TreeNode) {
                node = ((TreeNode&lt;K, V&gt;) p).getTreeNode(hash, key);
            } else {
                // 4. 如果节点类型是链表,就执行链表的查找逻辑
                do {
                  if (e.hash == hash &amp;&amp;
                            ((k = e.key) == key ||
                                    (key != null &amp;&amp; key.equals(k)))) {
                        node = e;
                        break;
                  }
                  p = e;
                } while ((e = e.next) != null);
            }
      }
      // 5. 当找到节点时,执行删除
      if (node != null &amp;&amp; (!matchValue || (v = node.value) == value ||
                (value != null &amp;&amp; value.equals(v)))) {
            if (node instanceof TreeNode) {
                ((TreeNode&lt;K, V&gt;) node).removeTreeNode(this, tab, movable);
            } else if (node == p) {
                // 删除的是头节点
                tab = node.next;
            } else {
                // 删除的是中间或尾节点
                p.next = node.next;
            }
            ++modCount;
            --size;
            afterNodeRemoval(node);
            return node;
      }
    }
    return null;
}
</code></pre>
<p>关于 <code>ConcurrentModificationException</code>:HashMap 的迭代器是 <strong>fail-fast</strong> 的。在创建迭代器时会记录当前的 <code>modCount</code> 值,每次调用 <code>next()</code> 时都会检查 <code>modCount</code> 是否与预期值一致。如果不一致(说明在迭代过程中代码修改了 HashMap 的结构),就会抛出 <code>ConcurrentModificationException</code>。这是 Java 集合框架的一种快速失败机制,目的是尽早发现并发修改问题,而不是等到数据错乱后才暴露。</p>
<h2 id="时间复杂度分析">时间复杂度分析</h2>
<h3 id="各操作时间复杂度详细对比">各操作时间复杂度详细对比</h3>
<table>
<thead>
<tr>
<th style="text-align: left">操作</th>
<th style="text-align: left">方法</th>
<th style="text-align: left">平均时间复杂度</th>
<th style="text-align: left">最坏时间复杂度</th>
<th style="text-align: left">空间复杂度</th>
<th style="text-align: left">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">插入</td>
<td style="text-align: left"><code>put</code></td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(log n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">无冲突时直接定位桶位 O(1),冲突严重时树化后 O(log n)</td>
</tr>
<tr>
<td style="text-align: left">查询</td>
<td style="text-align: left"><code>get</code></td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(log n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">同上</td>
</tr>
<tr>
<td style="text-align: left">删除</td>
<td style="text-align: left"><code>remove</code></td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(log n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">同上</td>
</tr>
<tr>
<td style="text-align: left">扩容</td>
<td style="text-align: left"><code>resize</code></td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">需要遍历所有元素并迁移到新数组,n 为元素总数</td>
</tr>
<tr>
<td style="text-align: left">树化</td>
<td style="text-align: left"><code>treeifyBin</code></td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">遍历链表构造 TreeNode 并构建红黑树</td>
</tr>
<tr>
<td style="text-align: left">退化</td>
<td style="text-align: left"><code>untreeify</code></td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">遍历红黑树转回链表,n 为桶位节点数</td>
</tr>
<tr>
<td style="text-align: left">遍历</td>
<td style="text-align: left"><code>forEach</code>/<code>entrySet</code></td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">遍历整个 table 数组 + 所有链表/树节点</td>
</tr>
<tr>
<td style="text-align: left">containsKey</td>
<td style="text-align: left"><code>containsKey</code></td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(log n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">等价于 <code>get</code> 再判断是否为 null</td>
</tr>
<tr>
<td style="text-align: left">containsValue</td>
<td style="text-align: left"><code>containsValue</code></td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">需要遍历所有桶位的所有节点</td>
</tr>
<tr>
<td style="text-align: left">size</td>
<td style="text-align: left"><code>size</code></td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">直接返回 <code>size</code> 字段</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>💡 核心提示</strong>:HashMap 的最坏情况 O(log n) 依赖于红黑树机制。如果 key 的 <code>hashCode()</code> 实现很差导致大量冲突,在 Java 7 中最坏情况是 O(n),Java 8 引入红黑树后才将最坏情况优化到 O(log n)。因此,<strong>自定义对象作为 key 时,务必正确实现 <code>hashCode()</code> 和 <code>equals()</code> 方法</strong>,这是保证 HashMap 性能的关键前提。</p>
</blockquote>
<h2 id="hashmap-核心参数与特性对比表">HashMap 核心参数与特性对比表</h2>
<h3 id="不同-map-实现对比">不同 Map 实现对比</h3>
<table>
<thead>
<tr>
<th style="text-align: left">特性</th>
<th style="text-align: left">HashMap</th>
<th style="text-align: left">LinkedHashMap</th>
<th style="text-align: left">TreeMap</th>
<th style="text-align: left">ConcurrentHashMap</th>
<th style="text-align: left">Hashtable</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left">底层结构</td>
<td style="text-align: left">数组+链表+红黑树</td>
<td style="text-align: left">数组+链表+红黑树+双向链表</td>
<td style="text-align: left">红黑树</td>
<td style="text-align: left">数组+链表+红黑树</td>
<td style="text-align: left">数组+链表</td>
</tr>
<tr>
<td style="text-align: left">有序性</td>
<td style="text-align: left">无序</td>
<td style="text-align: left">插入顺序/访问顺序</td>
<td style="text-align: left">按 key 排序</td>
<td style="text-align: left">无序</td>
<td style="text-align: left">无序</td>
</tr>
<tr>
<td style="text-align: left">线程安全</td>
<td style="text-align: left">否</td>
<td style="text-align: left">否</td>
<td style="text-align: left">否</td>
<td style="text-align: left">是(CAS+synchronized)</td>
<td style="text-align: left">是(synchronized)</td>
</tr>
<tr>
<td style="text-align: left">允许 null key</td>
<td style="text-align: left">是(仅1个)</td>
<td style="text-align: left">是(仅1个)</td>
<td style="text-align: left">否</td>
<td style="text-align: left">是(仅1个)</td>
<td style="text-align: left">否</td>
</tr>
<tr>
<td style="text-align: left">允许 null value</td>
<td style="text-align: left">是(多个)</td>
<td style="text-align: left">是(多个)</td>
<td style="text-align: left">否</td>
<td style="text-align: left">是(多个)</td>
<td style="text-align: left">否</td>
</tr>
<tr>
<td style="text-align: left">迭代器</td>
<td style="text-align: left">fail-fast</td>
<td style="text-align: left">fail-fast</td>
<td style="text-align: left">fail-fast</td>
<td style="text-align: left">weakly consistent</td>
<td style="text-align: left">Enumerator</td>
</tr>
<tr>
<td style="text-align: left">查询复杂度</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(log n)</td>
<td style="text-align: left">O(1)</td>
<td style="text-align: left">O(1)</td>
</tr>
<tr>
<td style="text-align: left">Java 版本</td>
<td style="text-align: left">1.2</td>
<td style="text-align: left">1.4</td>
<td style="text-align: left">1.2</td>
<td style="text-align: left">1.5</td>
<td style="text-align: left">1.0</td>
</tr>
</tbody>
</table>
<h3 id="核心常量对比">核心常量对比</h3>
<table>
<thead>
<tr>
<th style="text-align: left">常量名</th>
<th style="text-align: left">值</th>
<th style="text-align: left">作用</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: left"><code>DEFAULT_INITIAL_CAPACITY</code></td>
<td style="text-align: left">16</td>
<td style="text-align: left">默认初始容量</td>
</tr>
<tr>
<td style="text-align: left"><code>MAXIMUM_CAPACITY</code></td>
<td style="text-align: left">2^30</td>
<td style="text-align: left">最大容量限制</td>
</tr>
<tr>
<td style="text-align: left"><code>DEFAULT_LOAD_FACTOR</code></td>
<td style="text-align: left">0.75</td>
<td style="text-align: left">默认负载因子</td>
</tr>
<tr>
<td style="text-align: left"><code>TREEIFY_THRESHOLD</code></td>
<td style="text-align: left">8</td>
<td style="text-align: left">链表转红黑树阈值</td>
</tr>
<tr>
<td style="text-align: left"><code>UNTREEIFY_THRESHOLD</code></td>
<td style="text-align: left">6</td>
<td style="text-align: left">红黑树退化链表阈值</td>
</tr>
<tr>
<td style="text-align: left"><code>MIN_TREEIFY_CAPACITY</code></td>
<td style="text-align: left">64</td>
<td style="text-align: left">树化所需最小数组容量</td>
</tr>
</tbody>
</table>
<h2 id="生产环境避坑指南">生产环境避坑指南</h2>
<h3 id="1-预估容量避免频繁扩容">1. 预估容量,避免频繁扩容</h3>
<p>HashMap 每次扩容都会重新分配数组并迁移所有元素,这是一个 O(n) 的操作。如果生产环境中数据量已知,应该在构造时就指定合适的容量。</p>
<pre><code class="language-java">// 推荐:预估 1000 个元素
// 公式:capacity = expectedSize / loadFactor + 1
int capacity = (int) Math.ceil(1000 / 0.75) + 1; // = 1334 → tableSizeFor 后为 2048
Map&lt;String, Object&gt; map = new HashMap&lt;&gt;(capacity);
</code></pre>
<h3 id="2-正确重写-hashcode-和-equals">2. 正确重写 hashCode 和 equals</h3>
<p>自定义对象作为 key 时,<strong>必须同时重写 <code>hashCode()</code> 和 <code>equals()</code></strong>,否则会出现 key 能 put 但 get 不到的诡异问题。</p>
<pre><code class="language-java">// 错误示例:只重写 equals,不重写 hashCode
class User {
    String name;
    @Override
    public boolean equals(Object o) {
      if (o instanceof User) return name.equals(((User) o).name);
      return false;
    }
    // 缺少 hashCode,使用 Object 的默认实现(基于内存地址)
}

// 结果:两个 "name" 相同的 User 对象,hashCode 不同,会被当成两个 key
</code></pre>
<blockquote>
<p><strong>💡 核心提示</strong>:Java 规范明确规定:如果两个对象 <code>equals()</code> 返回 true,则它们的 <code>hashCode()</code> 必须相同。违反这个约定,HashMap 的行为将是未定义的。</p>
</blockquote>
<h3 id="3-遍历中删除使用-iterator">3. 遍历中删除使用 Iterator</h3>
<pre><code class="language-java">// 错误示例:遍历时直接调用 map.remove()
for (String key : map.keySet()) {
    if (key.startsWith("temp")) {
      map.remove(key); // ConcurrentModificationException!
    }
}

// 正确示例:使用 Iterator.remove()
Iterator&lt;String&gt; it = map.keySet().iterator();
while (it.hasNext()) {
    if (it.next().startsWith("temp")) {
      it.remove();
    }
}

// 或者使用 removeIf(Java 8+)
map.keySet().removeIf(key -&gt; key.startsWith("temp"));
</code></pre>
<h3 id="4-大-key-对象导致内存泄漏">4. 大 key 对象导致内存泄漏</h3>
<p>HashMap 的 key 会长期持有对象的引用。如果 key 是重量级对象,且 map 作为 static 字段长期存在,会导致内存泄漏。建议使用 <code>WeakHashMap</code> 或者适时清理。</p>
<pre><code class="language-java">// 使用 WeakHashMap,key 在 GC 时会被自动回收
Map&lt;BigObject, Value&gt; map = new WeakHashMap&lt;&gt;();
</code></pre>
<h3 id="5-高并发场景使用-concurrenthashmap">5. 高并发场景使用 ConcurrentHashMap</h3>
<p>HashMap 不是线程安全的。多线程并发 <code>put</code> 可能导致:</p>
<ul>
<li>数据覆盖(丢失更新)</li>
<li>size 不准确</li>
<li>JDK 7 中并发扩容可能形成链表环(Java 8 已修复此问题,但并发数据不一致仍存在)</li>
</ul>
<pre><code class="language-java">// 高并发推荐方案
Map&lt;String, Object&gt; map = new ConcurrentHashMap&lt;&gt;();

// 如果需要统计访问顺序,可以使用
ConcurrentHashMap&lt;String, Object&gt; map = new ConcurrentHashMap&lt;&gt;(16, 0.75f, 1);
</code></pre>
<h3 id="6-注意-integerlong-缓存陷阱">6. 注意 Integer/Long 缓存陷阱</h3>
<pre><code class="language-java">// Integer 缓存范围:-128 ~ 127
Map&lt;Integer, String&gt; map = new HashMap&lt;&gt;();
map.put(128, "a");
map.put(128, "b"); // key 是同一个 Integer 对象(自动装箱)

// 但如果用 new Integer(128),会创建不同对象
// 不过 HashMap 通过 equals 判断 key 是否相同,所以仍然正确
// 但作为 key 时,务必确保 hashCode 和 equals 的一致性
</code></pre>
<h3 id="7-监控-hashmap-大小">7. 监控 HashMap 大小</h3>
<p>在生产环境中,如果 HashMap 用作缓存但没有设置上限,可能无限增长导致 OOM。</p>
<pre><code class="language-java">// 使用 LRU 缓存方案替代
Map&lt;String, Object&gt; cache = new LinkedHashMap&lt;&gt;(16, 0.75f, true) {
    @Override
    protected boolean removeEldestEntry(Map.Entry&lt;String, Object&gt; eldest) {
      return size() &gt; 10000; // 超过 10000 时淘汰最久未访问的元素
    }
};
</code></pre>
<h2 id="总结">总结</h2>
<p>现在学完了 HashMap 底层源码实现,可以轻松回答开头的问题了。</p>
<h3 id="1-hashmap-的底层实现原理">1. HashMap 的底层实现原理</h3>
<p>答案:HashMap 的底层数据结构由数组、链表和红黑树组成,核心是基于数组实现的。为了解决哈希冲突,采用拉链法引入链表结构。为了解决链表过长导致的查询性能下降,Java 8 引入了红黑树结构。</p>
<h3 id="2-hashmap-的-put-方法执行流程">2. HashMap 的 put 方法执行流程</h3>
<p>答案:计算 hash → 数组为空则 <code>resize()</code> 初始化 → 计算下标 <code>(n-1) &amp; hash</code> → 下标为空则直接插入 → 下标不为空则判断 key 是否已存在 → 红黑树节点执行树插入 → 链表节点执行尾插 → 链表长度达 8 时树化 → <code>size &gt; threshold</code> 时扩容。</p>
<h3 id="3-hashmap-的扩容流程">3. HashMap 的扩容流程</h3>
<p>答案:容量翻倍(2 倍扩容) → 创建新数组 → 遍历旧数组,单节点直接迁移 → 链表通过 <code>hash &amp; oldCap</code> 拆分为低位和高位两条链表 → 红黑树执行 split 方法拆分。</p>
<h3 id="4-hashmap-为什么是线程不安全的">4. HashMap 为什么是线程不安全的?</h3>
<p>答案:因为 put、remove 等方法没有加同步锁,多线程并发操作时可能导致数据不一致、覆盖等问题。</p>
<p>想要实现线程安全,有三种方案:</p>
<ul>
<li>第一种:使用 <code>Hashtable</code>,每个方法都用 <code>synchronized</code> 加锁,线程安全但性能差。<pre><code class="language-java">Map&lt;Integer, Integer&gt; map = new Hashtable&lt;&gt;();
</code></pre>
</li>
<li>第二种:使用 <code>Collections.synchronizedMap()</code> 包装 HashMap,原理类似,也是用 <code>synchronized</code> 加锁。<pre><code class="language-java">Map&lt;Integer, Integer&gt; map = Collections.synchronizedMap(new HashMap&lt;&gt;());
</code></pre>
</li>
<li>第三种:使用 <code>ConcurrentHashMap</code>,采用 CAS + synchronized 细粒度锁,性能最好,也是推荐方案。<pre><code class="language-java">Map&lt;Integer, Integer&gt; map = new ConcurrentHashMap&lt;&gt;();
</code></pre>
</li>
</ul>
<h3 id="5-hashmap-的容量为什么设置成-2-的倍数并且是-2-倍扩容">5. HashMap 的容量为什么设置成 2 的倍数?并且是 2 倍扩容?</h3>
<p>答案:有三个原因:</p>
<ul>
<li><strong>加快哈希运算效率</strong>:如果容量不是 2 的幂,计算下标时只能通过对容量求余,即 <code>hash % n</code>(n 是容量)。如果容量是 2 的幂,可以通过位运算计算下标 <code>(n - 1) &amp; hash</code>,效率更高。</li>
<li><strong>散列更均匀</strong>:配合 <code>hash()</code> 方法中的高位异或,2 的幂容量能让键值分布更均匀,减少哈希冲突。</li>
<li><strong>扩容效率更高</strong>:扩容时链表通过 <code>hash &amp; oldCap</code> 拆分为两条链表,整体迁移,不需要重新计算每个元素的下标。</li>
</ul>
<h3 id="6-hashmap-在-java-8-版本中做了哪些变更">6. HashMap 在 Java 8 版本中做了哪些变更?</h3>
<p>答案:Java 8 对 HashMap 做了较大的重构,主要变更有:</p>
<ul>
<li>引入了红黑树结构,链表长度超过 8 时转换为红黑树,查询时间复杂度从 O(n) 优化到 O(log n)</li>
<li>优化了链表的扩容机制,原来需要重新计算每个节点下标,现在通过 <code>hash &amp; oldCap</code> 拆分为两条链表,整体迁移</li>
<li>扩容时机变化,原来添加元素前扩容,现在添加元素后扩容</li>
<li>链表插入方式变化,Java 7 之前采用头插法(多线程下可能形成环导致死循环),Java 8 开始采用尾插法</li>
</ul>
<h2 id="行动清单">行动清单</h2>
<ol>
<li><strong>检查生产环境 HashMap 初始化方式</strong>:审计代码中所有 <code>new HashMap&lt;&gt;()</code> 的调用,对于已知大小的场景改为指定初始容量,公式为 <code>(int) Math.ceil(expectedSize / 0.75) + 1</code>。</li>
<li><strong>审查自定义 key 对象</strong>:搜索所有自定义类作为 HashMap key 的场景,确认每个类都正确重写了 <code>hashCode()</code> 和 <code>equals()</code> 方法,且两者保持一致。</li>
<li><strong>排查并发场景误用</strong>:全局搜索 <code>HashMap</code> 在多线程上下文中的使用,替换为 <code>ConcurrentHashMap</code>,特别注意 static 字段的 HashMap。</li>
<li><strong>监控 HashMap 内存占用</strong>:为使用 HashMap 作为缓存的场景添加大小限制或使用 LRU 策略,防止无限增长导致 OOM;建议配置 JVM 参数 <code>-XX:+HeapDumpOnOutOfMemoryError</code> 以便排查。</li>
<li><strong>遍历中安全删除</strong>:审计所有遍历 HashMap 的代码,将 <code>for-each + map.remove()</code> 模式替换为 <code>Iterator.remove()</code> 或 <code>removeIf()</code>,避免触发 <code>ConcurrentModificationException</code>。</li>
<li><strong>扩展阅读</strong>:推荐阅读 JDK 源码中的 <code>HashMap</code> 完整实现,以及 Joshua Bloch 的《Effective Java》第 3 版第 11 条("始终在覆盖 equals 时覆盖 hashCode")。</li>
</ol><br><br>
来源:https://www.cnblogs.com/yidengjiagou/p/19903319
頁: [1]
查看完整版本: HashMap 源码深度解析