HashMap 就像一间会自动扩容的仓库,当货物(元素)太多导致仓库拥挤时,它会自动扩建仓库(扩容),让存取货物更高效。具体扩容规则如下:
一、何时触发扩容?
-
容量超阈值:
- 当前元素数量 > 容量(数组长度) × 负载因子(默认0.75)
- 示例:默认初始容量16,负载因子0.75,当放入第13个元素时(16×0.75=12),触发扩容。
-
链表过长转红黑树时(Java 8+):
- 当链表长度≥8,且数组容量<64时,优先扩容(而不是直接转红黑树)。
二、扩容过程
-
新容量计算:
- 旧容量 × 2(保持2的幂次,如16 → 32 → 64)。
-
数据迁移:
-
遍历旧数组的所有元素,重新计算每个元素的哈希值,分配到新数组的不同位置。
-
优化:由于容量是2倍,元素的新位置 = 原位置 或 原位置 + 旧容量。
- 例如:旧容量16(二进制10000),哈希值后4位决定位置;扩容后32(二进制100000),后5位决定位置。
-
-
耗时操作:
- 数据迁移需要遍历所有元素,频繁扩容会影响性能(应尽量预分配合理初始容量)。
三、为什么默认负载因子是0.75?
-
平衡空间与时间:
- 负载因子小(如0.5):扩容频繁,空间利用率低,但哈希冲突少。
- 负载因子大(如0.9):扩容不频繁,但哈希冲突多,链表/红黑树变长,查询变慢。
- 0.75是经验值,在时间和空间成本之间达到较好的平衡。
四、树化(链表转红黑树)的条件
- 链表长度≥8:单个链表过长,查询效率低(O(n))。
- 数组容量≥64:如果容量太小(如16),优先扩容而非树化。
树化目的:将链表转为红黑树,查询效率提升至O(log n)。
五、使用建议
-
预分配容量:
-
若预计存1000个元素,初始化容量设为2048(1000 / 0.75 ≈ 1333,取最近的2的幂次)。
-
代码示例:
int expectedSize = 1000; int initialCapacity = (int) (expectedSize / 0.75) + 1; Map<String, Integer> map = new HashMap<>(initialCapacity);
-
-
避免频繁扩容:
- 默认扩容会复制数据,影响性能,尤其是数据量大时。
六、总结
- 扩容条件:元素数量超过容量×负载因子。
- 扩容代价:数据迁移耗时,尽量预分配容量。
- 树化优先:容量小优先扩容,容量足够则转红黑树优化查询。
口诀:
「HashMap 像仓库,元素太多就扩建
负载因子零点七五,容量翻倍是常规
链表过长转红黑,预分配容更高效!」