一句话说透Java里面的HashMap何时扩容

186 阅读2分钟

HashMap 就像一间会自动扩容的仓库,当货物(元素)太多导致仓库拥挤时,它会自动扩建仓库(扩容),让存取货物更高效。具体扩容规则如下:


一、何时触发扩容?

  1. 容量超阈值

    • 当前元素数量 > 容量(数组长度) × 负载因子(默认0.75)
    • 示例:默认初始容量16,负载因子0.75,当放入第13个元素时(16×0.75=12),触发扩容。
  2. 链表过长转红黑树时(Java 8+):

    • 当链表长度≥8,且数组容量<64时,优先扩容(而不是直接转红黑树)。

二、扩容过程

  1. 新容量计算

    • 旧容量 × 2(保持2的幂次,如16 → 32 → 64)。
  2. 数据迁移

    • 遍历旧数组的所有元素,重新计算每个元素的哈希值,分配到新数组的不同位置。

    • 优化:由于容量是2倍,元素的新位置 = 原位置 或 原位置 + 旧容量。

      • 例如:旧容量16(二进制10000),哈希值后4位决定位置;扩容后32(二进制100000),后5位决定位置。
  3. 耗时操作

    • 数据迁移需要遍历所有元素,频繁扩容会影响性能(应尽量预分配合理初始容量)。

三、为什么默认负载因子是0.75?

  • 平衡空间与时间

    • 负载因子小(如0.5):扩容频繁,空间利用率低,但哈希冲突少。
    • 负载因子大(如0.9):扩容不频繁,但哈希冲突多,链表/红黑树变长,查询变慢。
    • 0.75是经验值,在时间和空间成本之间达到较好的平衡。

四、树化(链表转红黑树)的条件

  1. 链表长度≥8:单个链表过长,查询效率低(O(n))。
  2. 数组容量≥64:如果容量太小(如16),优先扩容而非树化。

树化目的:将链表转为红黑树,查询效率提升至O(log n)。


五、使用建议

  1. 预分配容量

    • 若预计存1000个元素,初始化容量设为2048(1000 / 0.75 ≈ 1333,取最近的2的幂次)。

    • 代码示例:

      int expectedSize = 1000;  
      int initialCapacity = (int) (expectedSize / 0.75) + 1;  
      Map<String, Integer> map = new HashMap<>(initialCapacity);  
      
  2. 避免频繁扩容

    • 默认扩容会复制数据,影响性能,尤其是数据量大时。

六、总结

  • 扩容条件:元素数量超过容量×负载因子。
  • 扩容代价:数据迁移耗时,尽量预分配容量。
  • 树化优先:容量小优先扩容,容量足够则转红黑树优化查询。

口诀
「HashMap 像仓库,元素太多就扩建
负载因子零点七五,容量翻倍是常规
链表过长转红黑,预分配容更高效!」