← 每日面试题 学习看板

HashMap是如何扩容的?

面试题/00java/01Java 基础/05 集合/HashMap是如何扩容的?.md

面试记录 2026-10-09

题目:HashMap 是如何扩容的?

难度:medium

标签:java/collections, general/knowledge

评分:⭐⭐☆☆☆(2/5)

我的回答

hashMap 的扩容,首先是要判断节点数,有多少节点了,达到多大量级进行扩容。扩容的触发条件应该是 75%,我记得有一个参数可以设置扩容比例默认是 0.75,当前容量达到了这个量级之后进行扩容。

点评

优点:

不足:

补充:

完整讲解

第一部分:知识重点剖析

核心考点定位:验证对 HashMap 扩容机制的完整掌握,尤其是 JDK8 相对 JDK7 的位运算优化,以及红黑树退化规则。

知识点拆解:

  1. 底层原理:HashMap 用负载因子控制"空间 vs 时间"折中。当 size > threshold 时扩容,避免链表过长导致 O(n) 查找。默认 loadFactor=0.75 是泊松分布下的经验值。
  2. 扩容流程(JDK8 resize):①判断是否首次初始化(table==null)→ newCap=initialCapacity;②否则 newCap=oldCap<<1(最大 1<<30);③若达 MAX_CAP 只增大 threshold;④重算 threshold=(int)(newCap*loadFactor);⑤遍历旧表迁移所有元素到新表。
  3. JDK8 迁移优化:对每个节点 e,只需判断 e.hash & oldCap:为 0 则留在原 index(因为新表容量翻倍后 index 位不变),非 0 则搬到 index+oldCap。这是利用"容量是 2 的幂"这一约束——新旧 index 只在旧低位置的基础上多了一位。
  4. 红黑树处理:树形桶扩容时调用 treeifyBin → split,若树 size < 6(TREEIFY_THRESHOLD-1)退化回链表,因为扩容后冲突已减少。
  5. 性能成本:每次扩容 O(n) 遍历全部元素;多次小步扩容累积开销大,所以预估容量时主动传入 new HashMap<>(expected/0.75f+1) 可显著减少 GC 与 CPU 抖动。
按「底层原理 → 设计权衡 → 工程实践」递进排列。

第二部分:考察方向与延伸递进

考察方向:

延伸递进面试题:

  1. e.hash & oldCap == 0 为什么就留在原 index?
  1. ConcurrentHashMap 如何协助扩容?
  1. 为什么负载因子默认 0.75 而不是 1.0 或 0.5?
  1. HashMap 大量扩容时怎么优化?

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,HashMap 的扩容由 resize() 方法完成,触发条件是 size > threshold,threshold 默认是容量 × 0.75。默认容量 16、loadFactor 0.75,所以第一次扩容阈值是 12。

扩容的具体过程 JDK7 和 JDK8 差别很大。JDK7 每次扩容都重新计算每个元素的 hash 值定位到新桶,然后用头插法挂接,这在并发场景下会形成环形链表导致 CPU 100%。

JDK8 做了两个关键优化。第一个是尾插法,保留了原链表的顺序,不再形成环。第二个是位运算迁移——不用重新算 hash,只需判断 e.hash & oldCap:如果结果是 0,说明元素 hash 的第 n 位是 0,扩容后新 index 等于旧 index,元素留在原位;如果结果非 0,说明第 n 位是 1,元素要搬到 index + oldCap 位置。这个优化的原理是容量是 2 的幂,新旧桶号只有在第 n 位上有差别,所以一个 bit 判断就能决定新位置。

扩容时如果桶是红黑树,会调用 split() 把树节点分拆到新旧两个桶;扩容后如果树 size 小于 6(TREEIFY_THRESHOLD - 1),会退化回链表,因为桶分散后冲突降低。

最大容量是 1 << 30,达到后不再扩容,只把 threshold 设为 Integer.MAX_VALUE 避免溢出。

工程上的最佳实践是预估容量时主动传入构造器,比如预估 1000 个元素就 new HashMap<>(1334)(1000/0.75+1 向上取 2 的幂前值),这样避免多次扩容抖动,能省掉大约 50% 的初始化时间开销。如果场景是并发写多,直接选 ConcurrentHashMap,它有协助扩容机制让多线程一起迁移。