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

面试考察点

  1. 设计理解深度:面试官不仅仅是想知道 “0.75” 这个数字,更是想知道你是否理解这个数值背后时间和空间的权衡思想,以及 Hash 冲突概率与负载因子的关系。
  2. 数学原理认知:考察你是否了解 0.75 与泊松分布的关系,以及为什么这个值能让哈希桶长度超过 8 的概率极低(约千万分之一)。
  3. 调优意识:如果你只知道默认值但不知道何时该调整,说明对 HashMap 的理解还停留在 “会用” 层面。

核心答案

0.75 是 空间利用率 和 查询效率 之间的最佳平衡点,这个值来自数学上的权衡分析: **** **** **** ****

角度 说明
空间换时间 负载因子越小,空间浪费越多,但哈希冲突越少,查询越快
时间换空间 负载因子越大,空间利用率越高,但哈希冲突越多,查询越慢
泊松分布 0.75 时,链表长度 ≥ 8 的概率约千万分之一,正好是转红黑树的阈值
历史经验 0.75 是经过大量实践验证的经验值,在大多数场景下表现良好

一句话总结:0.75 在空间利用率和查询效率之间取得了最佳平衡,同时保证了链表转红黑树的阈值(8)是合理的。

深度解析

一、空间与时间的权衡

负载因子 空间利用率 哈希冲突概率 查询效率 扩容频率 特点
0.5 低 低 高 高 16 容量存 8 个元素就扩容,空间浪费 50%,冲突极少,查询 O(1) 但内存消耗大
0.75(默认,推荐) 75% 可控 接近 O(1) 适中 16 容量存 12 个元素才扩容,空间利用率与冲突的折中,内存适中
1.0 高(100%) 高 低 低 16 容量存满 16 个元素才扩容,冲突很多,查询可能退化为 O(n)

上图展示了负载因子对 HashMap 性能的多维度影响。关键理解:

  • 负载因子小(如 0.5):相当于 “房间很大,人很少”,虽然每个人都有独立空间,但大量空间被浪费。对于 HashMap 来说,这意味着频繁扩容、内存消耗大。
  • 负载因子大(如 1.0):相当于 “房间很挤”,虽然空间利用率高,但冲突概率大幅增加。极端情况下,所有元素都哈希到同一个桶,查询退化为链表遍历 O(n)。
  • 0.75 的意义:相当于 “房间 75% 满”,既有较好的空间利用率,又能将冲突概率控制在可接受范围内。

二、泊松分布与红黑树阈值

这是 0.75 选择的数学原理,也是面试中的加分项:

负载因子 = 0.75 时,理想情况下每个桶平均有 0.75 个元素,按 Poisson 分布公式 P(k) = (λ^k / k!) * e^(-λ)(其中 λ = 0.75)计算各链表长度出现的概率:

链表长度 k 概率 P(k) 含义
0 0.4724 47% 的桶为空
1 0.3543 35% 的桶有 1 个元素
2 0.1329 13% 的桶有 2 个元素
3 0.0332 3% 的桶有 3 个元素
4 0.0062 0.6% 的桶有 4 个元素
5 0.0009 0.09% 的桶有 5 个元素
6 0.0001 0.01% 的桶有 6 个元素
7 0.00001 约十万分之一的概率
8 0.000001 约百万分之一的概率
≥ 8 极低 约千万分之一的概率

结论:链表长度达到 8 的概率极低,一旦出现说明哈希函数严重冲突或被恶意攻击,这正是 TREEIFY_THRESHOLD = 8(链表转红黑树阈值)的由来。

上图揭示了 0.75 与红黑树阈值(8)之间的数学关系。核心逻辑:

  • 泊松分布的应用:在负载因子 0.75 下,假设哈希函数足够随机,每个桶中的元素数量服从参数 λ = 0.75 的泊松分布。
  • 为什么阈值是 8? 从表格可以看出,链表长度达到 8 的概率约为百万分之一。这意味着正常情况下,链表几乎不可能达到长度 8。如果达到了,说明:
    • 哈希函数设计有问题
    • 或者有人恶意构造哈希冲突攻击(HashDoS 攻击)
  • 转红黑树的意义:当链表长度 ≥ 8 时,说明已经出现异常情况,此时将链表转为红黑树,将查询复杂度从 O(n) 降为 O(log n),防止性能急剧下降。

三、源码中的定义

// HashMap 源码中的定义
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {

/**
* The load factor used when none specified in constructor.
* 构造函数未指定时使用的默认负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* The bin count threshold for using a tree rather than list for a bin.
* 将链表转换为树的阈值(当桶中元素 >= 8 时)
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* The bin count threshold for untreeifying a (split) bin during
* a resize operation. Should be less than TREEIFY_THRESHOLD.
* 在扩容时,将红黑树转换回链表的阈值(当桶中元素 <= 6 时)
*/
static final int UNTREEIFY_THRESHOLD = 6;
}

源码中可以看到,DEFAULT_LOAD_FACTOR = 0.75f 和 TREEIFY_THRESHOLD = 8 是配套设计的。

四、什么场景需要调整负载因子?


场景 建议值 原因
内存紧张 0.8 ~ 0.9 牺牲查询效率,换取更高空间利用率
查询频繁、性能敏感 0.5 ~ 0.6 减少哈希冲突,保证查询效率
元素数量可预估 自定义 一次性设置 initialCapacity = 预估数量 / 0.75 + 1,避免扩容
缓存场景 0.75(默认) 平衡性能和内存,无需调整
// 示例:预知需要存储 1000 个元素,避免扩容
int expectedSize = 1000;
int capacity = (int) (expectedSize / 0.75) + 1; // 1334
Map<String, String> map = new HashMap<>(capacity);

面试高频追问

  1. 负载因子可以大于 1 吗?
    • 可以!负载因子 > 1 意味着元素数量可以超过容量
    • 但这会导致大量哈希冲突,查询效率急剧下降
    • 一般不建议超过 1.0
  2. 为什么链表转红黑树的阈值是 8,而红黑树转链表的阈值是 6?
    • 中间差 2 是为了避免频繁转换(链表 ↔ 红黑树)导致的性能抖动
    • 类似于 ArrayList 扩容 1.5 倍的 “滞后” 设计思想
  3. 如果负载因子改成 0.5,对性能有什么影响?
    • 空间利用率从 75% 降到 50%,内存消耗增加约 50%
    • 哈希冲突减少,查询效率略有提升
    • 扩容频率翻倍,插入性能可能下降

常见面试变体

  • “HashMap 的负载因子可以修改吗?什么场景需要调整?”
  • “为什么链表长度达到 8 时才转红黑树,而不是 4 或 16?”
  • “负载因子越大越好还是越小越好?”

记忆口诀

0.75 的由来:空间时间要平衡,泊松分布算概率,链表 8 是千万分之一,默认 0.75 刚刚好。

总结

HashMap 默认负载因子 0.75 是 空间利用率与查询效率的黄金平衡点。从数学角度看,0.75 配合泊松分布,使得链表长度 ≥ 8 的概率约为千万分之一,这正是 JDK 1.8 将链表转红黑树阈值设为 8 的依据。内存紧张时可适当调高,查询敏感时可适当调低。


声明:本文转载自小哈的博客 《为什么 HashMap 的默认负载因子要设置成 0.75?》,版权归原文作者所有。转载请保留本声明与原文链接。