HashMap的hash方法是如何实现的

在深入理解 HashMap 的整体设计之前,先看看它是如何将任意对象的 hashCode() 值,转换成数组索引可用的“桶下标”——也就是 HashMap 中的 hash(Object key) 方法。

/**
 * Computes key.hashCode() and spreads (XORs) higher bits of hash
 * to lower. Because the table uses power-of-two length, sets of
 * hashes that vary only in bits above the current mask will
 * always collide. (…若只用低位,则高位变化的 hash 会落到同一桶)
 */
static final int hash(Object key) {
    int h;
    // key.hashCode() 可能是 int 全 32 位,(h = key.hashCode()) 先取其原始值
    // 然后将 h 右移 16 位,并与原 h 做异或,
    // 将高 16 位的信息“扩散”到低 16 位,增强低位的随机性
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

一、为什么要对 hashCode() 做“扰动”?

  1. hashCode() 分布不一定理想

    • 不同类、不同实现的 hashCode() 分布可能偏向高位或低位集中。

    • 如果直接用 hashCode() 的低几位作为桶下标,而这些位又缺乏随机性,就会导致大量键落到同一桶上,触发冲突。

  2. HashMap 桶数组长度总是 2 的幂

    • 在计算数组下标时,HashMap 用的是 (n - 1) & hash,相当于取 hash 的低 log₂n 位。

    • 举例:当数组长度 n = 16 时,只取 hash 的低 4 位。

    • 如果原始 hashCode() 的低 4 位一致,无论高位如何不同,都将冲突落到同一个桶。

为了避免以上问题,JDK 将 hashCode() 的高 16 位通过“右移并异或”的方式,混合进低 16 位。这样,即使原始 hashCode() 只在高位有所区别,也能反映到低位,降低冲突概率。


二、hash() 方法详解

static final int hash(Object key) {
    int h;
    // 如果 key 为 null,则直接返回 0(所有 null 键同样映射到桶 0)
    // 否则,先取 key.hashCode(),赋值给局部变量 h
    // 再计算 h ^ (h >>> 16)
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • h = key.hashCode()
    调用对象自身的 hashCode() 方法,获得一个 32 位的初始哈希值。

  • h >>> 16
    无符号右移 16 位,将高 16 位移到低 16 位的位置,并补 0。

  • h ^ (h >>> 16)
    把原始的 h 与右移后的 h 做位异或(XOR),混合了高位信息到低位,也保留了低位原有的信息。


三、示例:高位差异如何影响低位

int[] codes = {
    // 假设 key1.hashCode() = 0x12340001
    0x12340001,
    // key2.hashCode() = 0x12350001,高位只差 0x00010000
    0x12350001
};

for (int hc : codes) {
    int mixed = hc ^ (hc >>> 16);
    System.out.printf("原始: 0x%08X, 混合后: 0x%08X\n", hc, mixed);
}

运行结果大致为:

原始: 0x12340001, 混合后: 0x12350001
原始: 0x12350001, 混合后: 0x12360001

可以看到,即使原始哈希只在第 17 位(0x00010000)有差异,经过扰动后,低 16 位也产生了不同,从而在取桶下标时会分散到不同的桶里。


四、与 JDK 7 的对比

  • JDK 7:在 HashMap 实现中,也做了扰动(supplementalHash()),但代码更复杂,且不同版本略有差异;

  • JDK 8:简化为上述的一行关键逻辑,更高效、更易理解。


五、为什么不扰动更多轮?

  • 性能权衡:每次扰动都要额外几条位运算指令,hash() 是高频调用,必需尽量精简;

  • 足够效果:一次 16 位混合已足以将高 16 位信息带入,极大改善低位分布;

  • 简单易维护:单次异或右移既高效又易于理解。


六、在实际使用中的影响

  1. 减少哈希冲突

    • 对于多数自定义 hashCode() 实现,都能更均匀地分布到桶中,保持接近 O(1) 性能。

  2. 抗 DoS 攻击

    • 即便攻击者构造一批只在高位或低位相同的 key,仍会被扰动分散到不同桶,降低碰撞风险。

  3. 兼容性

    • 对已有的 hashCode() 不做破坏性更改,只是在 HashMap 侧做一层灰盒优化,无需用户改动。


七、完整代码 & 注释

public class HashMap<K,V> {
    // ... 省略其他成员 …

    /** 阻止高碰撞:混合高 16 位至低 16 位 */
    static final int hash(Object key) {
        int h;
        // null 键恒定映射到桶 0
        if (key == null) {
            return 0;
        }
        // 调用 key.hashCode(),混合高 16 位信息,再返回
        h = key.hashCode();
        return h ^ (h >>> 16);
    }

    // 计算桶下标:取模 table.length
    static int indexFor(int hash, int length) {
        // length 必为 2^n
        return hash & (length - 1);
    }

    // put 操作示意
    public V put(K key, V value) {
        int hash = hash(key);
        int idx = indexFor(hash, table.length);
        // 在 table[idx] 链表或树中,查找或插入节点…
    }
}

八、总结

  • HashMap.hash() 方法通过一次右移异或,将 hashCode() 的高位信息“扩散”到低位,避免了电商、社交等高并发场景中因 hashCode() 分布不均导致的桶级拥堵;

  • 简洁高效的实现,兼顾性能与抗攻击性,是 Java 8 中 HashMap 能在绝大多数场景下保持稳定 O(1) 操作的重要一环。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值