发现问题
最近在处理一个线上问题时,遇到了非常奇怪的情况。
使用Math.abs(hash) % list.size()计算索引值时,竟然出现了负数!
看了下hash的值是-2147483648,恰好是Integer.MIN_VALUE。
按照绝对值的负负得正,结果应该是2147483648 % list.size(),结果是[0, list.size() - 1]。
但是结果是负的,这简直让人匪夷所思,难道Math.abs函数有问题吗?
本着技术人的思维,事出反常必有妖,咱们今天就来抓一下这个妖。
分析问题
直接找源码:
/**
* Returns the absolute value of an {@code int} value.
* If the argument is not negative, the argument is returned.
* If the argument is negative, the negation of the argument is returned.
*
* <p>Note that if the argument is equal to the value of
* {@link Integer#MIN_VALUE}, the most negative representable
* {@code int} value, the result is that same value, which is
* negative.
*
* @param a the argument whose absolute value is to be determined
* @return the absolute value of the argument.
*/
public static int abs(int a) {
return (a < 0) ? -a : a;
}
代码没啥问题,数组小于0的时候,取反,大于等于0的时候,返回原值。
细心的已经开始看注释了。
官方注释解释了,如果参数是Integer.MIN_VALUE,返回的还是自身。现象和描述相符,但是和代码不符,按照代码运行应该是-Integer.MIN_VALUE,负负得正。
玄机在哪呢?
没错,就是你想的那样。
你答对了
越界了。
int类型占32位,数值范围是[-2147483648, 2147483647]。-Integer.MIN_VALUE是2147483648,比Integer.MAX_VALUE的2147483647大,所以越界了。
在计算机系统中,数值一律用补码来表示和存储。使用补码的主要优点是可以将符号位和数值位统一处理,同时加法和减法也可以统一处理。
补码系统中,正数的补码就是其本身,负数的补码是在其原码的基础上,符号位不变,其余各位取反,最后加1。
在补码表示法中,Integer.MIN_VALUE的二进制表示是10000000000000000000000000000000,取反后的结果是01111111111111111111111111111111,再加一变成10000000000000000000000000000000,这正好是Integer.MIN_VALUE本身。
因此,Math.abs(Integer.MIN_VALUE)的结果仍然是Integer.MIN_VALUE,即-2147483648。
解决问题
如何避免这个问题呢?两种方式:
- 扩展数值范围,在调用Math.abs方法是,把hash转为Long类型,这个时候就不会越界了;
- 判断hash值是否是Integer.MIN_VALUE,如果是,直接返回0。
总结问题
是不是有朋友会想,JDK中居然有这么低级错误的API。不过,这次的错误和JDK中居然也有反模式接口常量的不一样,这次是“抽象泄漏法则”的一种情况。
什么是抽象泄漏法则?
“抽象泄漏”是软件开发时,本应隐藏实现细节的抽象化不可避免地暴露出底层细节与局限性。抽象泄露是棘手的问题,因为抽象化本来目的就是向用户隐藏不必要公开的细节。
由于软件开发与运行环境越来越复杂,开发者必须依赖于各种抽象。使得开发者专注于高层次的领域相关的知识与技能,以提高工作效率。
但是,抽象泄漏法则指出“可靠”软件的开发者必须了解抽象之下的底层细节。否则一旦出了任何问题,根本不会知道是怎么回事,也不知道如何除错或回复。
程序设计工具抽象掉某些东西,但和其他所有抽象机制一样都有漏洞,而唯一能适当处理漏洞的方法,就是弄懂该抽象原理以及所隐藏的东西。
所以抽象机制虽然节省了工作的时间,不过学习的时间是省不掉的。
比如本次的问题,Math.abs函数本身是一个简单的抽象,用于计算一个数的绝对值。然而,由于 Integer.MIN_VALUE和反码的特殊性质,Math.abs(Integer.MIN_VALUE)仍然返回负数,这暴露了底层整数表示的细节。
这种抽象的泄漏使得我们在使用Math.abs(hash) % list.size()计算索引值时遇到了意料之外的问题。为了了解其中的原因,我们得需要知道数值在计算机中的表示形式。
所以,工具虽然好用,碰到问题也得挠头。
少年,做好成为高级程序员的准备了吗?