hash() 以及(n-1)&hash的算法

总结:

  1. hashmap的容量为什么是 2 的 n 次方?

将容量设为2的n次方,是为了保证 n-1 的二进制低位全为 1,从而确保哈希值的每一个低位都能参与到最终的索引计算中,让数据能“均匀”地散布到数组的所有位置。

  1. 扰动函数的作用

hashCode高位信息参与到了最终的哈希桶定位计算中, 即使两个对象的原始 hashCode 只有高位不同而低位相同,经过这番“扰动”后,它们最终计算出的哈希桶索引也大概率会变得不同。

源码

 public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

 static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

  if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //通过putVal方法中的(n - 1) & hash决定该Node的存储位置
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);

第一步:理解 h >>> 16 这个操作的含义和作用。

这个操作叫做“无符号右移16位”。它的作用非常直观:就是将一个二进制数(在这里是 hashCode 的32位二进制表示)的所有位都向右移动16个位置。

我们以你提到的对象 A 为例:

  • 对象 A 的 hashCode (我们称之为 h) 是: 1000010001110001 0000011110000000

    • 为了方便观察,我将它分成了两部分:左边是“高16位”,右边是“低16位”。

执行 h >>> 16 操作后,这个二进制数会变成:

  • 0000000000000000 1000010001110001

发生了什么?
原本在“高16位”的部分,被整体移动到了“低16位”的位置上。而原本的“低16位”和移出来的“高16位”超出的部分,都被丢弃了。左边空出的16个位置则全部用0来填充。

请问,对于这一步——“通过右移16位,来获取原始 hashCode 的高16位部分”,您是否理解了?如果理解了,我们再进行下一步,解释为什么要这么做以及异或运算(^)的作用。

第二步:理解 ^ (异或) 运算在这里的作用。

这个操作叫做“按位异或”,符号是 ^。它的规则正如你所说:对两个二进制数的每一个位进行比较,如果两个位不同(一个0一个1),结果就是1;如果两个位相同(都是0或都是1),结果就是0。

现在,我们将第一步的结果和原始的 hashCode 值进行异或运算,也就是 h ^ (h >>> 16)

我们还是用对象 A 的 hashCode 举例:

  1. 原始 h:
    1000010001110001 0000011110000000 (高16位 | 低16位)
  2. h >>> 16:
    0000000000000000 1000010001110001 (高16位移到低16位后的结果)

现在,我们把这两个二进制数按位进行 ^ 运算:

  1000010001110001 0000011110000000   (原始 h)
^ 0000000000000000 1000010001110001   (h >>> 16)
-----------------------------------
  1000010001110001 1000001111110001   (最终的 hash() 方法返回值)

请注意观察结果的后16位(低16位)

  • 原始 h 的低16位是: 0000011110000000
  • 最终结果的低16位是: 1000001111110001

看到了吗? 原始 hashCode 的高16位信息(1000...)通过异或运算, “混合”或者说“注入” 到了低16位中,从而改变了低16位的值。

现在请思考:这个“混合”操作解决了什么问题?

请问,对于这一步——“通过异或运算,将原始 hashCode 的高位信息和低位信息混合在一起,得到一个新的、更混乱的哈希值”,您是否理解了?如果理解了,我们再进行最后一步,看看这个新的哈希值如何避免我们最初提到的问题。

说得非常到位!“更加混乱”正是关键所在。这个过程也常被称为“扰动函数”(Disturbance Function),目的就是增加随机性。

现在我们进入最后一步。

第三步:将新的哈希值代入 (n - 1) & hash 公式,看看效果如何。

默认的hash 是 key.hashCode()

回顾一下我们最初遇到的问题:

  • 当数组长度 n 为16时,n - 1 就是15,二进制是 0000...00001111
  • & 运算的特点是:只有当两个位都是1时,结果才是1。
  • 这意味着,无论 hash 值是什么, (n - 1) & hash 运算的结果只取决于 hash 值的低4位,因为 n - 1 的高位全是0。

这正是问题的根源:直接使用 a.hashCode() 时,只有低位的几个比特参与了最终位置的计算,高位信息被完全浪费了,这极易导致冲突。

现在,我们使用了 hash() 方法处理后的新哈希值:

  1. 原始 a.hashCode() 的低16位: 0000011110000000
  2. hash 值的低16位: 1000001111110001 (这是高16位和低16位混合后的结果)

现在我们再来计算对象A的索引位置:

  • (16 - 1) & (新hash值)
  • 也就是 ...00001111 & ...11110001

取两个二进制数的低4位进行 & 运算:

1111 & 0001 = 0001,结果是1。

我们再来看看对象B的情况(为了对比,我们假设B的高位和低位差异很大):

  • 原始 b.hashCode(): 0111011100111000 101000010100000
  • 经过 h ^ (h >>> 16) 混合后,它的新哈希值的低16位会变成: 0111011100111000 ^ 101000010100000 = 1101011101111000
  • 取新哈希值的低4位 1000,与 1111 进行 & 运算:1111 & 1000 = 1000,结果是8。

结论:
通过 h >>> 16^ 这两个看似复杂的操作,HashMap 成功地让 hashCode高位信息参与到了最终的哈希桶定位计算中。即使两个对象的原始 hashCode 只有高位不同而低位相同,经过这番“扰动”后,它们最终计算出的哈希桶索引也大概率会变得不同。

这就大大降低了哈希冲突的概率,使得数据在哈希表中分布得更加均匀。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值