核心概念

HashMap 的扩容机制(resize) 是指当元素数量超过阈值时,自动将底层数组容量扩大为原来的 2 倍,并将所有元素重新分配到新数组的过程。这是保证 HashMap 性能的关键机制。

扩容触发条件

1. 元素数量超过阈值

核心公式:

threshold = capacity × loadFactor

// 触发条件
if (++size > threshold) {
    resize();
}

默认值:

static final int DEFAULT_INITIAL_CAPACITY = 16;     // 默认容量
static final float DEFAULT_LOAD_FACTOR = 0.75f;    // 默认负载因子

// 默认阈值 = 16 × 0.75 = 12
// 插入第 13 个元素时触发扩容

2. 初始化时的延迟扩容

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    // 注意:此时 table 为 null,没有分配数组空间
}

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

final V putVal(int hash, K key, V value, ...) {
    Node<K,V>[] tab;
    
    // 首次 put 时,table 为 null,触发初始化扩容
    if ((tab = table) == null || tab.length == 0)
        tab = resize();  // ← 初始化扩容
    
    // ...
}

3. 树化前的容量检查

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n;
    
    // 如果容量 < 64,优先扩容而不是树化
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();  // ← 树化前扩容
    else {
        // 容量够了才树化
        // ...
    }
}

扩容完整流程

源码解析(JDK 1.8)

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) {
        // 情况1:已有容量,执行扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 已达最大容量(2^30),不再扩容
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY) {
            // 容量翻倍,阈值翻倍
            newThr = oldThr << 1;  // double threshold
        }
    }
    else if (oldThr > 0) {
        // 情况2:通过构造函数指定了初始容量
        newCap = oldThr;
    }
    else {
        // 情况3:首次初始化,使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY;  // 16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  // 12
    }
    
    // 计算新阈值(针对某些特殊情况)
    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) {
                    // 情况1:桶中只有一个节点
                    newTab[e.hash & (newCap - 1)] = e;
                }
                else if (e instanceof TreeNode) {
                    // 情况2:红黑树节点
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                }
                else {
                    // 情况3:链表节点(核心优化)
                    Node<K,V> loHead = null, loTail = null;  // 低位链表
                    Node<K,V> hiHead = null, hiTail = null;  // 高位链表
                    Node<K,V> next;
                    
                    do {
                        next = e.next;
                        // 通过 hash & oldCap 判断位置
                        if ((e.hash & 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);
                    
                    // 将低位链表放到原位置
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 将高位链表放到新位置(原位置 + oldCap)
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    
    return newTab;
}

扩容机制的核心优化

1. 位运算快速定位新位置

JDK 1.7 的方式(重新计算):

// 旧方式:每个元素都要重新计算 hash
int newIndex = hash % newCapacity;

JDK 1.8 的优化(位运算判断):

// 新方式:通过位运算快速判断
if ((e.hash & oldCap) == 0) {
    newIndex = oldIndex;          // 位置不变
} else {
    newIndex = oldIndex + oldCap; // 位置变化
}

原理说明:

假设 oldCap = 16 (0001 0000), newCap = 32 (0010 0000)

元素的旧位置: hash & (oldCap - 1) = hash & 0000 1111
元素的新位置: hash & (newCap - 1) = hash & 0001 1111

关键判断: hash & oldCap(即 hash 的第 5 位)
- 如果第 5 位 = 0: 新位置 = 旧位置
- 如果第 5 位 = 1: 新位置 = 旧位置 + 16

示例1: hash = 0000 0101 (5)
旧位置: 5 & 15 = 5
判断: 5 & 16 = 0 (第 5 位是 0)
新位置: 5 & 31 = 5 (不变)

示例2: hash = 0001 0101 (21)
旧位置: 21 & 15 = 5
判断: 21 & 16 = 16 (第 5 位是 1)
新位置: 21 & 31 = 21 = 5 + 16 (改变)

性能优势:

  • 不需要重新计算 hash
  • 只需一次位运算判断
  • 时间复杂度从 O(n × hash计算) 降低到 O(n)

2. 高低位链表分离

图示说明:

扩容前 (oldCap = 16):
table[5] → A(5) → B(21) → C(37) → D(5)

扩容后 (newCap = 32):
判断每个节点的 hash & 16:
- A(5):  5 & 16 = 0   → 低位链表
- B(21): 21 & 16 = 16 → 高位链表
- C(37): 37 & 16 = 32 → 高位链表
- D(5):  5 & 16 = 0   → 低位链表

分离结果:
低位链表: A → D
高位链表: B → C

新数组:
table[5]  = A → D       (原位置)
table[21] = B → C       (原位置 + 16)

代码实现:

// 构建低位链表和高位链表
Node<K,V> loHead = null, loTail = null;  // 低位链表(位置不变)
Node<K,V> hiHead = null, hiTail = null;  // 高位链表(位置改变)

do {
    next = e.next;
    if ((e.hash & 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);

// 放置到新数组
newTab[j] = loHead;           // 原位置
newTab[j + oldCap] = hiHead;  // 新位置

优势:

  • 保持原链表的相对顺序(JDK 1.7 的头插法会逆序)
  • 一次遍历完成分离,效率高
  • 避免了 JDK 1.7 的并发死循环问题

3. 红黑树的扩容处理

final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
    TreeNode<K,V> b = this;
    // 同样分为低位树和高位树
    TreeNode<K,V> loHead = null, loTail = null;
    TreeNode<K,V> hiHead = null, hiTail = null;
    int lc = 0, hc = 0;  // 统计节点数
    
    for (TreeNode<K,V> e = b, next; e != null; e = next) {
        next = (TreeNode<K,V>)e.next;
        e.next = null;
        
        if ((e.hash & bit) == 0) {
            // 低位树
            if ((e.prev = loTail) == null)
                loHead = e;
            else
                loTail.next = e;
            loTail = e;
            ++lc;
        }
        else {
            // 高位树
            if ((e.prev = hiTail) == null)
                hiHead = e;
            else
                hiTail.next = e;
            hiTail = e;
            ++hc;
        }
    }
    
    if (loHead != null) {
        // 如果低位树节点数 <= 6,退化为链表
        if (lc <= UNTREEIFY_THRESHOLD)
            tab[index] = loHead.untreeify(map);
        else {
            tab[index] = loHead;
            if (hiHead != null)
                loHead.treeify(tab);  // 重新树化
        }
    }
    if (hiHead != null) {
        // 如果高位树节点数 <= 6,退化为链表
        if (hc <= UNTREEIFY_THRESHOLD)
            tab[index + bit] = hiHead.untreeify(map);
        else {
            tab[index + bit] = hiHead;
            if (loHead != null)
                hiHead.treeify(tab);  // 重新树化
        }
    }
}

扩容性能分析

1. 时间复杂度

扩容操作总时间复杂度:O(n)

  • 遍历所有元素:O(n)
  • 位运算判断位置:O(1)
  • 链表分离:O(链表长度)
  • 红黑树处理:O(k log k),k 为树节点数

2. 空间复杂度

扩容过程的内存峰值:

扩容前: oldCap × 空间
扩容中: oldCap × 空间 + newCap × 空间 = 3 × oldCap × 空间
扩容后: newCap × 空间 = 2 × oldCap × 空间

峰值出现在数据迁移过程中

3. 扩容次数计算

public class ResizeCountTest {
    public static void main(String[] args) {
        // 默认容量 16,负载因子 0.75
        Map<Integer, String> map = new HashMap<>();
        
        int[] resizePoints = {13, 25, 49, 97, 193, 385, 769, 1537};
        
        for (int i = 0; i < 10000; i++) {
            map.put(i, "value" + i);
        }
        
        // 扩容次数 = log₂(10000/12) ≈ 10 次
        // 16 → 32 → 64 → 128 → 256 → 512 → 1024 → 2048 → 4096 → 8192 → 16384
    }
}

公式:

扩容次数 ≈ log₂(n / (初始容量 × 负载因子))

示例:插入 10000 个元素,初始容量 16
扩容次数 = log₂(10000 / 12) ≈ 9.7 ≈ 10 次

性能优化实践

1. 预估容量,减少扩容

// ❌ 错误:频繁扩容
Map<Integer, String> map1 = new HashMap<>();
for (int i = 0; i < 10000; i++) {
    map1.put(i, "value" + i);
}
// 触发约 10 次扩容

// ✅ 正确:预估容量
int expectedSize = 10000;
int capacity = (int) (expectedSize / 0.75 + 1);
Map<Integer, String> map2 = new HashMap<>(capacity);
for (int i = 0; i < 10000; i++) {
    map2.put(i, "value" + i);
}
// 只触发 1 次或 0 次扩容

2. 容量计算工具方法

public class HashMapUtils {
    /**
     * 根据预期元素数量计算初始容量
     * @param expectedSize 预期元素数量
     * @return 建议的初始容量
     */
    public static int calculateCapacity(int expectedSize) {
        return calculateCapacity(expectedSize, 0.75f);
    }
    
    public static int calculateCapacity(int expectedSize, float loadFactor) {
        if (expectedSize < 0) {
            throw new IllegalArgumentException("Expected size must be positive");
        }
        // 计算需要的容量
        int capacity = (int) Math.ceil(expectedSize / loadFactor);
        // 向上取 2 的幂次方
        return tableSizeFor(capacity);
    }
    
    private static int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= (1 << 30)) ? (1 << 30) : n + 1;
    }
    
    public static void main(String[] args) {
        System.out.println(calculateCapacity(100));   // 128
        System.out.println(calculateCapacity(1000));  // 2048
        System.out.println(calculateCapacity(10000)); // 16384
    }
}

3. 监控扩容性能

public class ResizePerformanceMonitor {
    public static void main(String[] args) throws Exception {
        int[] sizes = {100, 1000, 10000, 100000};
        
        for (int size : sizes) {
            testResize(size, false);  // 不预设容量
            testResize(size, true);   // 预设容量
            System.out.println("---");
        }
    }
    
    static void testResize(int size, boolean presize) throws Exception {
        Map<Integer, String> map = presize 
            ? new HashMap<>((int)(size / 0.75 + 1))
            : new HashMap<>();
        
        int resizeCount = 0;
        int lastCapacity = 0;
        
        long start = System.nanoTime();
        
        for (int i = 0; i < size; i++) {
            map.put(i, "value" + i);
            
            int currentCapacity = getCapacity(map);
            if (currentCapacity > lastCapacity) {
                resizeCount++;
                lastCapacity = currentCapacity;
            }
        }
        
        long end = System.nanoTime();
        
        System.out.printf("大小: %6d, 预设容量: %5s, 扩容次数: %2d, 耗时: %6d ms%n",
            size, presize, resizeCount, (end - start) / 1_000_000);
    }
    
    static int getCapacity(Map<?, ?> map) throws Exception {
        java.lang.reflect.Field tableField = HashMap.class.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] table = (Object[]) tableField.get(map);
        return table == null ? 0 : table.length;
    }
}

输出示例:

大小:    100, 预设容量: false, 扩容次数:  4, 耗时:     2 ms
大小:    100, 预设容量:  true, 扩容次数:  0, 耗时:     1 ms
---
大小:   1000, 预设容量: false, 扩容次数:  7, 耗时:     5 ms
大小:   1000, 预设容量:  true, 扩容次数:  0, 耗时:     2 ms
---
大小:  10000, 预设容量: false, 扩容次数: 10, 耗时:    15 ms
大小:  10000, 预设容量:  true, 扩容次数:  0, 耗时:     8 ms

JDK 1.7 vs JDK 1.8 扩容对比

对比项 JDK 1.7 JDK 1.8
插入方式 头插法 尾插法
位置计算 重新计算 hash 位运算判断
元素顺序 扩容后逆序 保持原顺序
并发问题 可能死循环 不会死循环(但仍不安全)
性能 较慢 更快

面试答题总结

答题要点:

  1. 触发条件:size > capacity × loadFactor(默认 0.75)
  2. 扩容倍数:容量翻倍(2 倍)
  3. 核心流程:计算新容量 → 创建新数组 → 数据迁移
  4. JDK 1.8 优化:位运算快速定位、高低位链表分离、尾插法

加分项:

  • 详细说明位运算优化原理(hash & oldCap)
  • 对比 JDK 1.7 和 1.8 的扩容差异
  • 提到红黑树扩容时的退化机制
  • 给出预估容量的计算公式
  • 说明扩容的时间和空间复杂度

一句话总结:HashMap 通过容量翻倍、位运算优化和高低位链表分离实现高效扩容,建议预估容量以避免频繁扩容。