在 Java 后端开发面试或源码阅读中,HashMap 永远是避不开的核心。很多人能熟练地背出它的扩容阈值、红黑树化条件,但当深入到源码底层,看到诸如 e.hash & (newCap - 1)e.hash & oldCap 这样的位运算时,往往会陷入沉思。


一、 起源:为什么数组长度必须是 2 的次幂?

在散列表中,为了让数据均匀分布,最直观的想法是对哈希值进行取模(求余数)

$$\text{索引位置} = \text{hash} \pmod{\text{Capacity}}$$

然而,在 CPU 的底层底层执行中,除法和取模(%)是非常昂贵的算术操作(可能需要几十个时钟周期)。为了追求极致的性能,底层的数论定理为我们提供了一个完美的替代方案:

数学定理:当容量 $C$ 是 $2$ 的次幂($2^n$)时,对于任意整数 $H$,满足:

$$H \pmod C \equiv H \ \& \ (C - 1)$$

位运算(&)在 CPU 中只需要 1 个时钟周期!为了享受到这个性能红利,HashMap 在源码中将容量死死限制为 2 的次幂:

// 默认初始容量 16 (2的4次方)
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

裁剪器的魔术:hash & (newCap - 1)

以容量 16 为例,16 - 1 = 15(二进制:0000 1111)。任何 Hash 值与 15 进行 & 运算,高位都会被无情“抹零”,只有最后 4 位被保留下来:

  e.hash   : 1010 0101  (某个对象的Hash值)
& 15 (16-1): 0000 1111
-------------------------------
  结果     : 0000 0101  (十进制的 5,即数组索引)

裁剪出来的结果范围绝对在 0 ~ 15 之间,在数学上完全等同于取模,速度却快了成百上千倍。


二、 进阶:扩容时的核心优化 e.hash & oldCap

当 HashMap 触发扩容(resize())时,原数组中的链表节点面临着重新分配(Rehash)。在 JDK 7 中,程序需要对每个节点重新进行一次上面提到的计算,不仅耗时,在多线程下还容易形成死循环。

JDK 8 引入了一个极其巧妙的判断:(e.hash & oldCap) == 0

1. 搬家还是留守?

扩容是翻倍的。比如旧容量 oldCap = 16(二进制 0001 0000),新容量 newCap = 32

  • 容量为 16 时,看的是二进制的低 4 位
  • 容量为 32 时,看的是二进制的低 5 位

也就是说,决定一个节点扩容后去哪里的,仅仅取决于它 Hash 值多出来的左边那 1 位(二进制的第 5 位)是 0 还是 1。

2. 精准的按位照准

oldCap(16)的二进制刚好是 0001 0000,除了第 5 位是 1,其余全为 0。 因此,e.hash & oldCap 就像是一把精准的手电筒,只照看新增的这一位

  • 结果 == 0:说明新增的那一位是 0。扩容后重新计算位置,它依然会留在原下标位置(归为低位链表)。
  • 结果 != 0:说明新增的那一位是 1。扩容后它的新位置必然是 原下标 + oldCap(归为高位链表)。
假设节点 A 和 B 之前都在下标 5:

节点 A 的 Hash: ...0000 0101  ->  A & 16 = 0         -> 留守原位(下标 5)
节点 B 的 Hash: ...0001 0101  ->  B & 16 = 16 (非0)  -> 搬家(5 + 16 = 下标 21)

三、 总结:HashMap 扩容全景流程

基于上述的高低位拆分优化,HashMap 的 putresize 流程变得无比丝滑。

1. Put 方法核心步骤

  1. 计算哈希:利用扰动函数 (h = key.hashCode()) ^ (h >>> 16) 将高位特征混合到低位。
  2. 定位桶位:通过 hash & (capacity - 1) 快速定位。
  3. 解决冲突:桶为空直接放入;不为空则遍历链表或红黑树。若链表长度 $\ge 8$ 且数组长度 $\ge 64$ 则触发树化。
  4. 检查扩容:当元素总量超过阈值(capacity * loadFactor)时,启动 resize()

2. Resize 扩容数据迁移步骤

graph TD
    A[开始扩容 resize] --> B["创建新数组 newTable, 容量翻倍"]
    B --> C[遍历旧数组的每个桶位]
    C --> D{桶位节点类型?}

    D -- 单节点 --> E["重新计算索引: e.hash & (newCap - 1) 
放入新数组"] D -- 红黑树 --> F[调用 split 拆分树] D -- 链表 --> G{"判断 (e.hash & oldCap) == 0"} G -- 是 --> H["归入低位链表
位置不变: newTable[原位置]"] G -- 否 --> I["归入高位链表
搬家: newTable[原位置 + oldCap]"] E --> J{是否遍历完?} F --> J H --> J I --> J J -- 否 --> C J -- 是 --> K[扩容完成]

结语:工程美学的最高体现

HashMap 的设计者 Joshua Bloch 等大牛,通过强制容器大小为 2 的次幂 这一空间规律,成功用 位运算 换取了时间的极致性能。

& (cap - 1) 的精简截取,到 & oldCap 的高低位完美拆分,HashMap 的源码向我们展示了什么是真正的计算机工程美学——将严谨的数学定理,转化为压榨硬件底层的极致武器。