HashMap

HashMap 源码分析

HashMap 用「数组 + 链表 + 红黑树」实现平均 O(1) 的键值存取:hash 定位桶、冲突挂链、链太长转树兜底,最坏从 O(n) 封顶到 O(log n)。
允许一个 null 键、多个 null 值,非线程安全。

代码块JAVA · 239 行收起展开
// 基于 JDK 25 (本地 D:/1ForCode/JAVA_Source), java.util.HashMap
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    // 默认初始容量 16。容量必须是 2 的幂,(n-1)&hash 才能等价取模(见 hash() 处的说明)
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    // 容量上限 2^30:int 正数范围内最大的 2 的幂
    static final int MAXIMUM_CAPACITY = 1 << 30;

    // 0.75 是空间利用率与冲突率的折中,size 超过 capacity*0.75 触发扩容
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 桶里已有 8 个节点还往里插,尝试转红黑树。0.75 负载下单桶到 8 的概率约亿分之六,
    // 正常几乎不触发:树化是给劣质 hashCode / 哈希碰撞攻击兜底的
    static final int TREEIFY_THRESHOLD = 8;

    // resize 拆分树时节点数 <= 6 退回链表。8/6 之间空 2,防止 size 在阈值附近抖动时反复互转
    static final int UNTREEIFY_THRESHOLD = 6;

    // 表容量 < 64 时不转树、先扩容:小表冲突多半是表太小,扩容摊薄比建树便宜
    static final int MIN_TREEIFY_CAPACITY = 64;

    // 桶数组,懒初始化,长度总是 2 的幂。transient:序列化走自定义 writeObject,只写键值对
    transient Node<K,V>[] table;

    transient int size;

    // 结构性修改计数,迭代器 fail-fast 依据(尽力而为,不能当并发保护用)
    transient int modCount;

    // 扩容阈值 = capacity * loadFactor。坑:带容量构造后、首次 resize 前,它临时存放初始容量
    int threshold;

    final float loadFactor;

    // 链表节点。hash 缓存下来,resize / 比较时不再重算
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        // ... 构造器与 Map.Entry 实现略
    }

    // 扰动函数:把 hashCode 高 16 位异或进低 16 位。定位桶只用低位,
    // 让高位也参与,否则只在高位有差异的一批 key 会整批撞进同一个桶
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

    // 返回 >= cap 的最小 2 的幂。JDK8 是五连移位或运算,JDK9 起一条 numberOfLeadingZeros 搞定
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    public HashMap(int initialCapacity, float loadFactor) {
        // ... 参数校验略(负容量、非法 loadFactor 抛异常,超 2^30 截断)
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity); // 初始容量暂存在 threshold,首次 resize 才换算成真阈值
    }

    public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // 只记负载因子,数组不分配,首次 put 才建表
    }

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;            // 懒初始化:建表也走 resize
        if ((p = tab[i = (n - 1) & hash]) == 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;                              // 先比 hash 再 equals,hash 不等就省掉一次 equals
            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);   // 尾插。JDK7 是头插,并发扩容会成环
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);  // 桶里已有 8 个、这次插的是第 9 个,尝试转树
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;                // putIfAbsent 只在旧值为 null 时覆盖
                afterNodeAccess(e);                 // 空方法,LinkedHashMap 用它维护访问顺序
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)                     // 先插入后判扩容,JDK7 反过来
            resize();
        afterNodeInsertion(evict);                  // 同为 LinkedHashMap 钩子,LRU 淘汰入口
        return null;
    }

    // 初始化或容量翻倍,整个 HashMap 最重的操作
    final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;      // 到 2^30 上限就不再扩,放任冲突
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;                        // 构造时暂存在 threshold 的初始容量在这里兑现
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;               // 断开旧表引用,帮 GC
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  // 树也按 lo/hi 拆,拆后 <=6 退回链表
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {   // 只看 hash 里新参与定位的那一位:0 留原下标
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {                          // 1 则整体挪到 j + oldCap
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;     // 尾接拆链保持相对顺序,全程不重算 hash
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(key)) == null ? null : e.value;
    }

    // JDK8 的签名是 getNode(int hash, Object key) 两参,后来把 hash 挪进了方法内
    final Node<K,V> getNode(Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n, hash; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & (hash = hash(key))]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;                       // 多数桶只挂一个节点,首节点命中是最快路径
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);   // 树上查找 O(log n)
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);     // 链上顺序找 O(链长)
            }
        }
        return null;
    }

    // 链表转树入口。表太小只扩容;真转树分两步:先换节点类型,再建树
    final void treeifyBin(Node<K,V>[] tab, int hash) {
        int n, index; Node<K,V> e;
        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            resize();
        else if ((e = tab[index = (n - 1) & hash]) != null) {
            TreeNode<K,V> hd = null, tl = null;
            do {
                TreeNode<K,V> p = replacementTreeNode(e, null); // Node 逐个替换成 TreeNode
                if (tl == null)
                    hd = p;
                else {
                    p.prev = tl;        // TreeNode 同时维护 prev/next 双向链,退化回链表时直接串回
                    tl.next = p;
                }
                tl = p;
            } while ((e = e.next) != null);
            if ((tab[index] = hd) != null)
                hd.treeify(tab);        // 到这里才真正建红黑树,染色和旋转都在 treeify 里
        }
    }
    // ... 序列化、视图、TreeNode 内部类等其余成员略
}

JDK 7 版对照

代码块JAVA · 73 行收起展开
// JDK 7 的 HashMap:数组 + 链表,没有红黑树。与 JDK8+ 的三个关键区别:
//   1. 结构:JDK8 链长到阈值转红黑树,JDK7 冲突再多也是一条链,最坏 O(n)
//   2. 插入:JDK7 头插,JDK8 尾插
//   3. 扩容:JDK7 逐节点重算下标 + 头插,多线程会成环;JDK8 lo/hi 拆链保序不成环
// 节点类叫 Entry(JDK8 改名 Node),常量与字段和 JDK8 相同,此处略。

    // 扰动做 4 次移位异或,比 JDK8 的一次 (h ^ h>>>16) 重。
    // JDK8 敢简化,是因为有红黑树兜底,扰动不必做满
    final int hash(Object k) {
        int h = 0;
        h ^= k.hashCode();
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    public V put(K key, V value) {
        if (table == EMPTY_TABLE)
            inflateTable(threshold);        // 同样懒初始化
        if (key == null)
            return putForNullKey(value);    // null 键单独一条路径,固定放 table[0];JDK8 靠 hash()=0 统一了
        int hash = hash(key);
        int i = indexFor(hash, table.length);   // indexFor = h & (length-1),同 JDK8 的 (n-1)&hash
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);       // 先扩容再插入,JDK8 反过来
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }

    // 头插:新节点直接当链头,插入 O(1),但链表顺序被反转,祸根在此
    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;
    }

    void resize(int newCapacity) {
        Entry<K,V>[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

    // 著名的坑:多线程并发 resize 形成环形链表,之后 get 命中该桶就死循环、CPU 100%。
    // 根因是逐节点头插反转顺序,两个线程交错执行时可能出现 A.next=B 且 B.next=A
    void transfer(Entry<K,V>[] newTable) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {
            while (null != e) {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);  // 每个节点重算下标
                e.next = newTable[i];                   // 头插进新桶
                newTable[i] = e;
                e = next;
            }
        }
    }
// 一句话:JDK7 = 头插 / 并发扩容成环;JDK8+ = 红黑树 / 尾插 / lo-hi 拆链不成环。两版都非线程安全。

原理串讲

一次 map.put(key, value) 的完整链路:入口先过 hash(key),把 hashCode 的高 16 位异或进低 16 位。
为什么要这一步?因为后面定位桶用的是 (n - 1) & hash,n 是 2 的幂时 n-1 的二进制全是低位 1,这个按位与等价于取模但快得多;代价是定位只看 hash 的低位,如果一批 key 的 hashCode 只在高位有差异(源码注释举的例子是连续整数值的 Float),它们会整批落进同一个桶。
花一次异或让高位也参与定位,等于用最便宜的方式把「速度换来的缺陷」补了回来。

进入 putVal,若 table 还是 null(new HashMap() 只记了 loadFactor,什么都没分配),第一次 put 由 resize() 顺手建表。

代码块JAVA · 2 行收起展开
为什么懒初始化?很多 Map 建出来从没装过东西,晚建表就不用白占一片数组。定位到桶后分三路:桶空直接 `newNode`;首节点 hash 相等且 equals 相等就记下待覆盖;首节点是 TreeNode 走 `putTreeVal` 树插入;否则顺链尾插,当桶里已有 8 个节点、这次插入第 9 个时调 `treeifyBin`。
`treeifyBin` 先看表容量:小于 64 只做 `resize()`,小表冲突多半是表太小,扩容摊薄比建树便宜;容量够了才把整条链换成 TreeNode 双向链,再由 `hd.treeify(tab)` 染色旋转建成红黑树。

插入完成后 ++modCount++size,超过 threshold 触发 resize()

resize() 的精髓在迁移。容量从 oldCap 翻倍后,每个节点的新下标只取决于 hash 里新参与运算的那一位:(e.hash & oldCap) == 0 的留在原下标 j,等于 1 的整体挪到 j + oldCap。
所以 JDK8 起迁移不重算任何 hash,只按这一位把链拆成 lo、hi 两条,尾接保序。
为什么坚持保序?JDK7 的 transfer 逐节点重算下标加头插,头插反转链表顺序,两个线程同时扩容时可能把 A.next 指向 B 的同时 B.next 又指向 A,之后 get 命中该桶就死循环。
JDK8 的拆链方式让并发扩容不再成环,但并发丢数据依旧,HashMap 从没承诺线程安全,只是不再拖垮 CPU。

get(key) 是同一套定位的只读版:getNode(key) 内部算 hash、(n - 1) & hash 定位桶,先查首节点。
多数桶只挂一个节点,这个快路径让平均查找就是一次 hash 加一次比较;首节点不中,TreeNode 走 getTreeNode 的 O(log n),链表顺序找 O(链长)。

最后一个为什么:转树阈值 8、退化阈值 6 怎么来的?源码注释给了泊松分布推算,0.75 负载下单桶节点数达到 8 的概率约亿分之六,正常的 hashCode 根本到不了。
树化的定位是兜底:对付写砸的 hashCode 和恶意构造的碰撞攻击。TreeNode 体积约是 Node 的两倍,常态用不起,所以阈值定在「几乎不触发」的位置;8 与 6 之间空出的 2 是缓冲,防止 size 在阈值附近抖动时链表和树来回互转。

设计取舍

  • 容量恒为 2 的幂:换来 (n-1)&hash 的定位速度,代价是必须靠 hash() 扰动补低位随机性。Hashtable 走相反路线,素数容量加取模。
  • threshold 字段身兼两职:带容量构造之后、首次 resize 之前,它存的是 tableSizeFor 算出的初始容量,读源码时别当成真阈值。
  • 树化是兜底手段,常态几乎不触发;key 没实现 Comparable 时,树内同 hash 节点靠 tieBreakOrder 用 identityHashCode 强行分出大小。
  • modCount 的 fail-fast 只是尽力而为,迭代中的结构修改不保证及时抛 ConcurrentModificationException,并发正确性要靠 ConcurrentHashMap。
  • 能预估元素数就用 HashMap.newHashMap(n)(JDK 19 起),它替你按 0.75 换算好容量;手写 new HashMap<>(n) 装 n 个元素,会在 0.75n 处扩一次容。

延伸阅读