Math.abs(Integer.MIN_VALUE) 的思考

1. 理解 Java 的 int​ 和 Integer.MIN_VALUE​

  • 32位有符号整数: 在 Java 中,int​ 类型是 32 位的有符号整数。这意味着它可以表示从 -2,147,483,648​ 到 +2,147,483,647​ 的整数。

  • 二进制补码 (Two's Complement): Java(以及大多数现代计算机)使用一种叫做“二进制补码”的方式来表示负数。在这种表示法中:

    • 正数和零的最高位(最左边的位,也叫符号位)是 0​。
    • 负数的最高位是 1​。
  • 值的范围不对称: 你会注意到,负数的范围比正数多一个。

    • ​Integer.MAX_VALUE​ (最大正数) 是 +2,147,483,647​。它的二进制表示是 0111 1111 1111 1111 1111 1111 1111 1111​ (一个 0 后面跟 31 个 1)。
    • ​Integer.MIN_VALUE​ (最小负数) 是 -2,147,483,648​。它的二进制表示是 1000 0000 0000 0000 0000 0000 0000 0000​ (一个 1 后面跟 31 个 0)。
  • 关键点: 没有一个正数的值是 +2,147,483,648​。int​ 类型无法表示这个数。

2. 为什么 Math.abs(Integer.MIN_VALUE)​ 仍然是负数?

  • ​Math.abs(x)​ 的定义是:如果 x​ 大于等于 0,返回 x​;如果 x​ 小于 0,返回 -x​。
  • ​Integer.MIN_VALUE​ (-2,147,483,648​) 显然小于 0。
  • 所以,Math.abs(Integer.MIN_VALUE)​ 应该返回 - (Integer.MIN_VALUE)​,也就是 - (-2,147,483,648)​,理论上应该是 +2,147,483,648​。
  • 但是! 正如我们上面所说,+2,147,483,648​ 这个数值无法用 32 位的 int​ 类型来表示。
  • 在二进制补码运算中,对 Integer.MIN_VALUE​ (1000...000​) 取负(按位取反再加 1),得到的结果还是它自己 (1000...000​)。这是一种溢出情况。
  • 因此,在 Java 中,执行 Math.abs(Integer.MIN_VALUE)​ 的结果仍然是 Integer.MIN_VALUE​,也就是 -2,147,483,648​。它并没有变成正数!

3. 为什么这是个风险?

  • 布隆过滤器的代码(以及很多需要数组索引的场景)期望哈希函数最终产生一个非负的索引值,通常在 0​ 到 数组大小 - 1​ 的范围内。
  • 如果哈希计算的中间结果(在 Math.abs​ 之后,但在最终取模或位运算之前)恰好是 Integer.MIN_VALUE​,那么它仍然是一个负数。
  • 将一个负数用作数组(或 BitSet​)的索引会导致 ArrayIndexOutOfBoundsException​ 错误,程序会崩溃。
  • 虽然原始代码 Math.abs(...) & (cap - 1)​ 中的 & (cap - 1)​ 部分(当 cap​ 是 2 的幂时)可能碰巧能处理负数输入并产生一个有效的索引,但这依赖于位运算的特性,而不是 Math.abs​ 真正保证了非负性。依赖这种巧合是不健壮的。

4. 解决方案:(hash & 0x7FFFFFFF) % cap​

这个方法分两步解决了问题:

  • 第一步:hash & 0x7FFFFFFF​ - 强制变为非负数

    • ​0x7FFFFFFF​ 是一个十六进制数,它的二进制表示是 0111 1111 1111 1111 1111 1111 1111 1111​。

    • 注意,这个数的最高位(符号位)是 0​,其余 31 位全是 1​。它正好就是 Integer.MAX_VALUE​。

    • ​&​ 是按位与 (Bitwise AND) 运算符。它会对两个数的每一位进行比较:只有当两个数的对应位都是 1​ 时,结果的该位才是 1​,否则是 0​。

    • 当任何一个 int​ 型的 hash​ 值与 0x7FFFFFFF​ 进行按位与操作时:

      • ​hash​ 的最高位(符号位)与 0x7FFFFFFF​ 的最高位 0​ 进行 AND 运算,结果永远是 0​。这意味着结果数的符号位一定是 0​,所以结果必然是一个非负数。
      • ​hash​ 的其余 31 位与 0x7FFFFFFF​ 的其余 31 个 1​ 进行 AND 运算,结果将保留 hash​ 原来的低 31 位 (因为 x & 1​ 结果是 x​)。
    • 效果: hash & 0x7FFFFFFF​ 这个操作巧妙地将任何 int​ 值(无论是正数、负数还是零,甚至是 Integer.MIN_VALUE​)都转换成一个非负数,同时尽可能地保留了原始哈希值的比特信息(除了符号位)。

  • 第二步:... % cap​ - 确保在范围内

    • 现在我们有了一个保证非负的哈希值(来自第一步)。
    • ​% cap​ 是取模运算符。它计算 (非负哈希值)​ 除以 cap​ 的余数。
    • 结果保证落在 0​ 到 cap - 1​ 的范围内,这正是数组或 BitSet​ 所需的有效索引范围。

总结:

​Math.abs()​ 对于 Integer.MIN_VALUE​ 无效,无法保证返回非负数。而 (hash & 0x7FFFFFFF)​ 利用位运算技巧,强制将任何 int​ 的符号位设为 0,从而绝对保证得到一个非负数。之后再使用 % cap​(或者在 cap​ 是 2 的幂时使用 & (cap - 1)​)将这个非负数映射到有效的索引范围内,这样就既安全又健壮了。

你可能感兴趣的:(哈希算法,算法,网络,java,开发语言,数据结构)