Java学习随笔:HashMap 扩容时到底发生了什么(2026-10-09)
HashMap 是我平时用得最多的集合之一,但很长一段时间里,我对它的理解只停留在“键值对、查询快”这一层。直到认真看了它的扩容逻辑,我才发现这个看似简单的容器背后有不少精心的设计。
先说容量。HashMap 的容量并不是随便一个整数,而是始终保持为 2 的幂。这样做的原因藏在它定位数组下标的方式里:它用键的哈希值对数组长度取模,来决定元素落到哪个桶里,而当长度是 2 的幂时,取模运算可以等价地换成一次按位与运算,效率更高。所以即使我们在构造时传入一个不是 2 的幂的容量,它也会自动向上取到最近的 2 的幂。
再说扩容的触发条件。HashMap 内部用一个阈值来控制,阈值大致等于容量乘以负载因子,默认负载因子是 0.75。当存放的元素个数超过这个阈值时,就会触发一次扩容,容量变成原来的两倍。之所以不是等到装满才扩容,是因为元素越多、哈希冲突越严重,查找会退化成遍历链表,因此需要在空间和时间之间找一个平衡点。
扩容的过程并不只是把数组变长。因为容量变了,原本每个元素该放在哪个下标也会跟着变,所以必须把旧数组里的元素重新分配到新数组里。这一步在较早的实现里是一次又一次重新计算哈希,而在现在的实现里做了一个很巧妙的优化:由于新容量是旧容量的两倍,元素的新位置只有两种可能,要么留在原来的下标,要么移动到“原下标加上旧容量”的位置。判断依据就是元素哈希值中新增的那一位是 0 还是 1,一次按位与就能决定,不需要重新计算完整的哈希。
还有一个细节值得注意:扩容时如果某个桶里已经是红黑树结构,节点数太少会退化成链表;反过来,链表的长度达到一定阈值又会转成红黑树。这样做的目的,是让极端情况下的查找性能不至于太差。
顺带说一句,多线程环境下这整套流程是不安全的。因为扩容要整体搬移数据,如果两个线程同时触发扩容,就可能互相覆盖,甚至让链表形成环,出现死循环。所以并发场景不该用 HashMap,而应该用并发容器。
理解扩容之后,我对 HashMap 的使用也有了新的认识。比如在能预估元素数量时,最好在构造时就指定一个合适的初始容量,避免中途反复扩容,既节省时间也减少内存抖动;再比如负载因子一般不要随便调大,虽然那样能减少扩容次数,但会显著增加冲突的概率。
这次学习让我体会到,很多“看起来就应该这么快”的东西,其实是靠一层层细节堆出来的。把扩容这件事想清楚,比背下“底层是数组加链表”这句话要有意义得多。

浙公网安备 33010602011771号