HashMap 扩容机制:深入理解底层原理

HashMap 作为 Java 中最常用的数据结构之一,其高效的查找性能得益于精巧的设计。 其中,扩容机制 是保证 HashMap 性能的关键因素之一。 当 HashMap 中的元素数量达到一定阈值时,就会触发扩容操作,以避免过多的哈希冲突,从而影响查找效率。 本文将深入探讨 HashMap 的扩容机制,帮助你更好地理解其底层原理。

1. 为什么需要扩容?

HashMap 的底层是一个数组,每个数组元素被称为一个 桶(bucket)。 当我们向 HashMap 中添加元素时,会根据 key 的哈希值计算出该元素应该放入哪个桶中。 然而,由于哈希函数的局限性,不同的 key 可能会产生相同的哈希值,导致 哈希冲突(Hash Collision)

当多个元素被放入同一个桶中时,这些元素会以链表(或红黑树)的形式存储在该桶中。 如果桶中的元素过多,会导致链表(或红黑树)过长,从而降低查找效率。 想象一下,如果 HashMap 中所有的元素都集中在一个桶中,那么查找元素的时间复杂度就会退化为 O(n),失去了 HashMap 的优势。

为了避免这种情况,HashMap 需要进行扩容操作。 扩容是指创建一个新的数组,其容量是旧数组的两倍,并将旧数组中的所有元素重新哈希到新数组中。 通过扩容,可以减少每个桶中的元素数量,从而降低哈希冲突的概率,提高查找效率。

2. 扩容的触发条件

HashMap 在以下两种情况下会触发扩容:

  • 达到负载因子: 当 HashMap 中键值对的数量超过 容量 * 负载因子 时,就会触发扩容。
    • 容量(Capacity): 指的是 HashMap 底层数组的长度。
    • 负载因子(Load Factor): 是一个介于 0 和 1 之间的浮点数,用于控制 HashMap 的拥挤程度。 默认值为 0.75。
    • 例如,如果 HashMap 的容量为 16,负载因子为 0.75,那么当键值对的数量超过 16 * 0.75 = 12 时,就会触发扩容。
  • 单个桶(bucket)的链表长度过长: 在 JDK 8 中,如果 HashMap 使用链表来解决哈希冲突,当链表的长度超过 8 且 HashMap 的容量小于 64 时,会优先进行扩容,而不是将链表转换为红黑树。

3. 扩容的具体步骤

  1. 计算新的容量:

    • 新的容量通常是旧容量的两倍。
    • HashMap 会检查新的容量是否超过最大容量(2^30),如果超过,则将容量设置为最大容量。
  2. 创建新的数组:

    • 创建一个新的数组,容量为新的容量。
    • 这个新数组将用于存储重新哈希后的键值对。
  3. 重新哈希(Rehashing):

    • 遍历旧数组中的每个桶(bucket)。
    • 对于每个桶中的键值对,重新计算它们的哈希值和索引位置。
    • 将键值对移动到新数组中对应的桶里。

    重新哈希的两种方式(JDK 8 优化):

    • 如果桶中只有一个节点(没有哈希冲突): 直接将该节点移动到新数组中对应的位置。
    • 如果桶中是一个链表或红黑树: 将链表或红黑树拆分成两个链表或红黑树,分别存储到新数组的不同位置。
      • 高低位链表/树: JDK 8 引入了高低位链表/树的优化。 根据每个键的哈希值在扩容后新增的 bit 位是 0 还是 1,将链表/树拆分成两个:
        • 低位链表/树: 新增的 bit 位为 0,索引位置不变,仍然在原来的索引位置。
        • 高位链表/树: 新增的 bit 位为 1,索引位置变为 原索引位置 + 旧容量
      • 这种方式可以减少重新计算哈希值的次数,提高扩容的效率。
  4. 替换旧数组:

    • 将新数组赋值给 HashMap 的内部数组,替换掉旧数组。
    • 旧数组会被垃圾回收器回收。

4. 扩容的开销

HashMap 的扩容是一个非常耗时的操作,因为它需要重新计算所有键值对的哈希值和索引位置,并将它们移动到新的数组中。 因此,应该尽量避免频繁的扩容。

5. 如何避免频繁扩容?

  • 选择合适的初始容量: 在创建 HashMap 时,可以根据预期的键值对数量选择合适的初始容量。 避免初始容量过小,导致频繁扩容。
  • 选择合适的负载因子: 可以根据实际情况调整负载因子。 负载因子越大,HashMap 的空间利用率越高,但哈希冲突的概率也会增加。 负载因子越小,HashMap 的空间利用率越低,但哈希冲突的概率也会降低。

6. 总结

HashMap 的扩容机制是保证其性能的关键因素之一。 通过扩容,可以减少哈希冲突的概率,提高查找效率。 扩容是一个耗时的操作,应该尽量避免频繁的扩容。 在实际使用中,我们可以通过选择合适的初始容量和负载因子来优化 HashMap 的性能。

关键词: Java, HashMap, 扩容, 哈希冲突, 负载因子, 重新哈希

参考资料:

声明: 本文仅为个人学习总结,如有错误,欢迎指正。


这篇博客详细介绍了 HashMap 的扩容机制,包括扩容的原因、触发条件、具体步骤、开销以及如何避免频繁扩容。 希望这篇文章能够帮助你更好地理解 HashMap 的底层原理,并在实际开发中更好地使用 HashMap。