jdk 1.8 扩容时通过 e.hash & oldcap 判断新桶位置:为0则留原索引,非0则置原索引+oldcap;不触发 treeifybin();链表用头插法拆分致顺序反转;threshold 更新为 newcap × loadfactor。

扩容时 resize() 怎么算新桶位置?
JDK 1.8 不再用 hash % newCapacity 或 hash & (newCapacity - 1) 重算所有键的哈希值,而是靠一个位移判断:只看新增的最高位比特是否为 1。因为容量始终是 2 的幂,扩容即左移一位(如 16→32),相当于多出一个高位 bit。
所以每个旧桶里的节点,要么留在原位置 index,要么移到 index + oldCap —— 只需判断 e.hash & oldCap 是否为非零:
- 若为 0 → 原位置不动
- 若非 0 → 新位置 = 原索引 +
oldCap
这个判断比重新哈希快得多,也避免了取模或额外位运算。但前提是 hash 值本身分布均匀,否则仍可能堆积。
treeifyBin() 在扩容时会触发红黑树转换吗?
不会。扩容过程中,即使某个桶已链化且长度 ≥ 8,resize() 也不会调用 treeifyBin()。它只做最简拆分:把原链表按 e.hash & oldCap 拆成两条子链(lo 链和 hi 链),然后分别插入新数组的对应位置。
红黑树转换只发生在 putVal() 的常规插入路径中,且满足两个条件:binCount >= TREEIFY_THRESHOLD(8) 且 table.length >= MIN_TREEIFY_CAPACITY(64)。扩容时哪怕旧桶是树,也会先 split() 成两棵子树,不降级也不升级。
为什么扩容后链表顺序可能反转?
因为 JDK 1.8 用的是头插法拆分链表:遍历旧链时,每个节点都插到对应子链(lo 或 hi)的头部。这导致子链内部顺序与原链相反。
- 例如原链:A → B → C,在 lo 链中变成 C → B → A
- 这不是 bug,是设计选择:避免尾插需要遍历查尾,性能更稳
- 但如果你代码依赖遍历顺序(比如靠插入顺序做隐式排序),这里会出问题
- 并发场景下更危险——多个线程同时 resize 可能成环(JDK 1.7 的经典死循环),1.8 虽用单线程 resize 规避了这点,但顺序反转仍是事实
扩容阈值 threshold 是怎么更新的?
每次 resize() 后,threshold 会设为 newCap * loadFactor。但注意:这个值不是“下次必须扩容的精确键数”,而是“当前容量下允许的最多键数”。实际触发扩容的条件是 size >= threshold,而 size 是键值对总数,不是桶数。
- 初始
threshold = 12(默认容量 16 × 0.75) - 第一次扩容后,容量变 32,
threshold = 24 - 如果中途删除大量元素,
size下降,但threshold不会自动下调——缩容要靠手动clear()或重建 - 所以长期增删频繁的 Map,可能一直维持大容量却低负载,浪费内存
位移算法省了哈希重算,但没解决“扩容易、缩难”的本质问题;真正影响性能的,往往是误估初始容量导致频繁 resize,而不是位移本身。









