负载因子 0.75 与阈值 threshold = capacity * loadFactor

30 阅读4分钟

1) 负载因子到底是什么

  • 记 capacity 为散列表数组长度(始终是 2 的幂),size 为已存键值对数量。

  • 负载因子 loadFactor 表示允许的装载密度:当 size > threshold 就扩容。

  • 阈值 threshold = capacity × loadFactor(取整为 int)。达到阈值触发 扩容(容量翻倍) + 低成本再分布

直觉:负载因子越大→更省内存,但每个桶平均元素更多,冲突上升;越小→更快更稳、但更占内存。


2) 为什么默认是 0.75?

  • 这是经验与概率分析的折中:在均匀散列假设下,桶内元素数近似泊松分布,平均链长 ≈ α = size/capacity

  • α = 0.75 时,平均每桶元素不到 1 个;既节省空间(不是 0.5 那么“空”),又能保持 get/put 接近 O(1)

  • 更小(如 0.5)→ 碰撞更少,但膨胀更频繁、内存更大;更大(如 0.9)→ 省内存但冲突显著上升,树化概率提高,整体延迟变差。

总结:0.75 在“时间/空间”之间的工程最优点,对一般业务是稳妥默认。


3) threshold 如何计算 & 何时会变

  • 正常运行时:threshold = capacity × loadFactor。
  • **扩容(容量翻倍)**时:新阈值 newThreshold = newCapacity × loadFactor。
  • 极值保护:当容量逼近 1<<30 时,阈值会被顶到 Integer.MAX_VALUE,停止再扩容。

4) 构造器的“初始容量陷阱”(JDK 8 关键点)

new HashMap(initialCapacity, loadFactor)
  • 传入的 initialCapacity 不是“立刻的数组长度” 。JDK 8 会先把它上取整到最近的 2 的幂暂存到 threshold 字段第一次 put 时才真正分配 table,并把 threshold 改成 capacity × loadFactor。

  • 这导致一个常见误会:仅传“预计元素个数”作为 initialCapacity 并不能保证不扩容

:new HashMap<>(1000)(loadFactor=0.75)

  • 第一次分配 capacity=1024,随后 threshold=1024×0.75=768。
  • 写到第 769 个就会扩容到 2048(阈值 1536)。

5) 如何“预估初始容量”避免扩容

目标是:让 threshold ≥ 预期元素个数

因此应满足:

capacity ≥ ceil(expectedSize / loadFactor)
realCapacity = nextPowerOfTwo(capacity)

Java 代码(可直接用):

static int initialCapacityFor(int expectedSize, float loadFactor) {
  int cap = (int)Math.ceil(expectedSize / loadFactor);
  int n = cap - 1;
  n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16;
  int pow2 = (n < 0) ? 1 : (n >= 1<<30) ? 1<<30 : n + 1;
  return pow2; // 传给 new HashMap<>(pow2)
}
  • 默认 0.75 下的简化:pow2(nextPowerOfTwo( ceil(expected×4/3) ))。
  • Kotlin:同理计算后传入构造函数即可。

6) 扩容节奏(感受一下)

以默认 0.75 为例(capacity → threshold):

  • 16 → 12

  • 32 → 24

  • 64 → 48

  • 128 → 96

    只要 size 超过阈值就翻倍。翻倍后再分布非常便宜:每个节点只会留在原桶或转移到原桶 + oldCap(因为 index = hash & (n-1),详见你之前问题的回答)。


7) 什么时候要改负载因子?

  • 内存吃紧、键分布好:可以略微调大(如 0.80~0.9),换取更少的内存与更少扩容次数——但要接受更多冲突与更高最坏延迟。
  • 对延迟/尾部性能很敏感(低卡顿、低 P99):可调小(如 0.5~0.66)减冲突,换更稳定的查询时间与更大内存。
  • 键分布差/容易碰撞(hashCode 质量不高、前缀相同等):别调大,甚至调小;更关键是修 hashCode/equals 与键不可变性

8) 实战建议 & 易错点

  1. 批量构建大 Map:先用上面的公式预估容量,一次到位,避免 N 次扩容抖动。
  2. 不要用原始类型 Map:会把类型错误推迟到运行时;与负载因子无关但常一起踩坑。
  3. 键必须不可变且 equals/hashCode 一致:否则再好的负载因子也救不了冲突/查不到/删不掉。
  4. 别“盲目 0.75 以外” :多数业务按默认就好;调参前先压测看扩容次数、桶碰撞率、P95/P99。
  5. 与 LinkedHashMap/ConcurrentHashMap:默认负载因子也常是 0.75,但并发/顺序语义不同,调参需单独压测。

一句话收束

负载因子 0.75 是 HashMap 在时间/空间上的经典折中;阈值 threshold = capacity × loadFactor 决定何时扩容。想避免扩容,就按 ceil(expected / loadFactor) 计算并上取 2 的幂作为初始容量;除非有明确的性能/内存诉求,否则按默认 0.75 就是最省心、最稳妥的选择。