为什么 HashMap 的默认负载因子要设置成 0.75?

#Java#面试八股

为什么 HashMap 的默认负载因子要设置成 0.75?

结论先行:0.75 是空间与时间的折中值。它让桶的平均占用率停在 3/4:既不会像 0.5 那样闲置一半数组、频繁扩容,也不会像 1.0 那样逼近满表、冲突激增。0.75 又是 3/4,与 16、32 这类常见容量相乘正好是整数,阈值计算不用四舍五入,工程实现也干净。

1. 负载因子到底控制什么

threshold = capacity × loadFactor。容量 16、负载因子 0.75,阈值就是 12;放入第 13 个元素时扩容到 32。负载因子越高,表越“满”才扩容,空间利用率高但桶内冲突多;越低则相反。

负载因子容量 16 时的阈值空间占用冲突概率扩容频率查询表现
0.58浪费明显低高快但内存代价大
0.7512较紧凑可控适中综合最优
1.016最紧凑高,链表变长低查询退化风险

2. 官方注释给出的概率依据

HashMap 源码注释里有一句关键说明:在随机 hash 下,桶内节点数近似服从泊松分布(Poisson distribution),默认扩容阈值(0.75)对应的参数 λ ≈ 0.5,桶内恰好有 k 个节点的概率为 e^-0.5 × 0.5^k / k!,前几项是:

桶内节点数 k概率
00.60653066
10.30326533
20.07581633
30.01263606
40.00157952
50.00015795
60.00001316
70.00000094
80.00000006

也就是说,正常哈希下桶里出现 8 个节点的概率约为亿分之六。这个数字同时解释了另一个默认值:TREEIFY_THRESHOLD = 8——树化几乎不会被误触发,但一旦真的触发,说明哈希分布已经病态,值得用红黑树兜底。

3. 为什么不是 1.0,也不是 0.5

  • 取 1.0:数组全满才扩容,桶内平均 1 个节点,冲突概率显著上升;链表变长后查询从 O(1) 向 O(n) 滑落,树化也会频繁发生。省下的内存换来的是 CPU 和延迟。
  • 取 0.5:一半桶位空着,扩容次数接近翻倍,每次都伴随 O(n) 的数组分配与迁移;对内存敏感的场景得不偿失。
  • 取 0.75:扩容次数与冲突率都落在可接受区间,且 0.75 = 3/4,与 16、32 等容量相乘都是整数(16×0.75=12、32×0.75=24),阈值不需要四舍五入。

4. 代码:观察扩容时机

import java.util.HashMap;
import java.util.Map;

public class LoadFactorDemo {
    public static void main(String[] args) {
        Map<Integer, Integer> map = new HashMap<>(16, 0.75f);
        for (int i = 1; i <= 12; i++) {
            map.put(i, i);
        }
        System.out.println(map.size()); // 输出:12,正好到阈值,尚未扩容

        map.put(13, 13);                // 第 13 个元素触发 resize:16 -> 32
        System.out.println(map.size()); // 输出:13

        // 估算容量:要装 1000 个元素,容量应取 1000 / 0.75 以上
        int expected = 1000;
        int capacity = (int) (expected / 0.75f) + 1; // 1334,tableSizeFor 后为 2048
        Map<String, Integer> big = new HashMap<>(capacity);
        System.out.println(big.size()); // 输出:0,数组在首次 put 时才分配
    }
}

HashMap 的容量与阈值是私有字段,JDK 9 之后反射读取还需要 --add-opens,因此这类验证通常靠行为观察而不是直接读字段。

5. 什么时候值得改负载因子

场景建议值理由
内存紧张、元素数量可预估0.8 ~ 0.9减少数组占用,代价是冲突略增
延迟敏感、内存充足0.5 ~ 0.6降低冲突,查询更稳定
普通业务代码保持 0.75调整收益远小于预分配容量
需要并发改用 ConcurrentHashMap它的负载因子固定为 0.75,构造参数只影响初始容量

注意 loadFactor 是 final 字段,构造之后无法修改;想换负载因子只能在创建时传入。

常见追问

  • 阈值会不会因浮点误差算错? (int)(newCap * loadFactor) 是截断取整;容量是 2 的幂且负载因子 0.75 时乘积精确,不会出现 11.999 这类误差。
  • 负载因子能大于 1 吗? 语法上可以,但桶内平均超过 1 个节点,冲突必然增多,一般没有意义。
  • 初始容量和负载因子是什么关系? 想装 n 个元素,容量至少 n / 0.75,否则必然扩容一次以上。
  • 为什么注释说 0.75 是 time 与 space 的折中? 高负载省空间费时间,低负载省时间费空间,0.75 是经验与概率共同选出的平衡点。
  • 0.75 与树化阈值 8 有什么联系? 8 是在 0.75 负载下“几乎不可能自然出现”的桶长度,正好用来区分正常冲突与病态哈希。

小结

0.75 不是拍脑袋的数字:它把桶的平均占用率控制在 3/4,让扩容次数和冲突率同时处于可接受区间;官方注释用 λ≈0.5 的泊松分布证明“桶内 8 个节点”的概率只有亿分之六,从而让 8 这个树化阈值既灵敏又不误伤。实际工作中不要折腾负载因子,把初始容量按 元素数 / 0.75 估准,收益更大。

笔记加载中…