深入解析 Java HashMap 内部扩容底层原理与流程(JDK 1.8 核心四部曲)
在 Java 开发与大厂面试中,HashMap 绝对是“常客”中的“霸主”。而提到 HashMap,其**底层的扩容机制(Resize)**更是高频必考题。
很多同学知道“容量到了 0.75 就扩容”、“扩容是原来的 2 倍”,但这往往无法打动面试官。今天,我们将基于 JDK 1.8 的源码逻辑,用最通俗易懂的语言和实例,把 HashMap 的扩容流程彻底掰碎了讲清楚!
整体扩容流程概览
HashMap 的扩容流程从宏观上看,主要分为三大步:
- 触发扩容(什么时候扩?)
- 创建新数组(扩多大?)
- 元素迁移核心四部曲(怎么搬家最快?)
下面我们逐一拆解。
步骤一:触发扩容的 2 大条件
什么情况下 HashMap 会觉得“太挤了”,需要换个大房子?主要有以下两种触发场景:
1. 常规触发(JDK 1.7 / 1.8 共用)
当我们向 HashMap 中不断 put 元素,发现:
当前元素总个数 >= 数组容量 × 负载因子(0.75) 时,就会触发扩容。
举个栗子🌰:
假设你刚new了一个默认的 HashMap,初始数组容量(Capacity)是 16。16 × 0.75 = 12。也就是说,当你 put 第 12 个(或者第 13 个,取决于具体边界判定)元素时,HashMap 就会自动触发扩容。
通俗理解: 公寓一共 16 个单间,住满了 12 个,为了防止后面来的人挤在同一个单间(哈希冲突过大),提前换个 32 个单间的大公寓。
2. 链表树化前的判断触发(JDK 1.8 特有)
JDK 1.8 引入了红黑树来优化长链表。当某个桶(槽位)上的链表长度 > 8 时,理论上要转成红黑树。
但是! 转换红黑树是有前提的:必须要求整个数组的长度 >= 64。
如果链表长度 > 8,但此时整个数组的长度还 < 64(比如还是 16 或 32),HashMap 会认为当前冲突严重不是因为单链太长,而是整个数组空间太小了。此时它不会转红黑树,而是直接触发扩容,利用扩容把这条过长的链表强行“打散”。
步骤二:创建新数组
一旦决定扩容,马上就会申请一块新的内存空间。
规则非常简单粗暴:创建一个长度是旧数组 2 倍的新数组。
为什么必须是 2 的整数次幂?
这是为了后续计算下标(hash & (length - 1))的极致效率,同时也是为了 JDK 1.8 中极其巧妙的“高低位数据拆分”打下基础(见下文)。
步骤三:元素迁移(核心四部曲✨)
新房子建好了,接下来就是把老房子里的租客(节点数据)搬到新房子里。
在 JDK 1.7 中,迁移数据需要重新计算每个节点的 Hash 下标,非常耗性能。
而在 JDK 1.8 中,大神 Doug Lea 给出了一个教科书级别的优化:利用 hash & 旧容量 来进行巧妙的高低位迁移拆分,全程无需重新计算 Hash 下标!
具体搬家操作分为以下四个步骤(遍历旧数组的每一个槽位):
第一步:空节点跳过
遍历旧数组,如果发现当前下标位置为空(null),说明压根没住人,直接跳过,看下一个槽位。
第二步:单节点直达新家
如果该位置只有一个普通的独立节点(没有链表也没有树),这是最简单的。
它的新家地址直接用:新下标 = hash & (newCap - 1) 计算得出,直接放进新数组对应位置即可。
第三步:链表节点的高低位拆分(神仙操作)
如果该位置是一条链表(好多个节点连在一起),怎么搬?
JDK 1.8 会拿出节点的 hash 值和旧数组容量(oldCap)做按位与(&)运算。
因为容量都是 2 的次幂(比如 16 就是二进制的 10000),所以 hash & 16 的结果只有两种可能:0 或者 16(即不为 0)。
利用这个特性,直接把一条链表拆成两条:
hash & oldCap == 0:这部分节点留在低位(low 链表)。- 新家地址:新下标 = 旧下标(位置完全不变)。
hash & oldCap != 0:这部分节点去往高位(high 链表)。- 新家地址:新下标 = 旧下标 + 旧数组容量。
通俗举例🌰:
原来的数组容量是 16。在下标5这个位置有一条链表,包含节点 A 和 B。
扩容到 32 后:
- 节点 A 算出来是 0,不动!它在新数组的下标依旧是
5。- 节点 B 算出来不是 0,搬迁!它在新数组的下标等于
5 + 16 = 21。
只需一次位运算,完美将长链表随机打散成两份!
第四步:红黑树节点的拆分与降级
如果该位置已经是一棵红黑树了,处理逻辑和链表非常类似。
同样使用上面的按位与方法,将一棵树分化为两棵树(低位 low 树 和 高位 high 树)。
分完之后,需要根据切分后树节点的大小决定它未来的命运:
- 如果分化后的树节点数 <= 6(注意是 6 不是 8):红黑树显得有些大材小用了,直接退化降级成普通链表。
- 如果分化后的树节点数 > 6:继续保留当初的傲骨,维持红黑树的形态。
最终落座规则和链表一样:low 树的节点在旧下标,high 树的节点在“旧下标 + 旧容量”。
总结与面试高分话术
在面试中如果被问到 HashMap 扩容,你可以这样总结:
“HashMap 扩容主要包含触发机制、两倍扩容和数据迁移三步。在 JDK 1.8 中最核心的亮点是其数据迁移机制的优化。它摒弃了 JDK 1.7 重新取模算下标的低效操作,转而通过
hash & oldCap的位运算,极其巧妙地把原有冲突的链表或红黑树拆分成高低位两部分。运算结果为 0 的留在原索引位置,不为 0 的移动到了原索引 + 旧容量的新位置。这不仅省去了重新计算 hash 的时间,而且将原来的冲突平均打散,大大提升了扩容的性能。同时在转移红黑树节点时,还会判断拆分后的节点数是否小于等于 6,从而触发红黑树向链表的退化降级。”
把这段话完整顺畅地答出来,面试官一定会对你刮目相看!
觉得有帮助的话,欢迎点赞收藏,持续跟进更多底层源码硬核解析!
更多推荐





所有评论(0)