負數(shù)的絕對值居然還是負數(shù),你知道嗎?
發(fā)現(xiàn)問題
最近在處理一個線上問題時,遇到了非常奇怪的情況。
使用Math.abs(hash) % list.size()計算索引值時,竟然出現(xiàn)了負數(shù)!
看了下hash的值是-2147483648,恰好是Integer.MIN_VALUE。
按照絕對值的負負得正,結(jié)果應該是2147483648 % list.size(),結(jié)果是[0, list.size() - 1]。
但是結(jié)果是負的,這簡直讓人匪夷所思,難道Math.abs函數(shù)有問題嗎?
本著技術人的思維,事出反常必有妖,咱們今天就來抓一下這個妖。
分析問題
直接找源碼:
/**
* 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;
}
代碼沒啥問題,數(shù)組小于0的時候,取反,大于等于0的時候,返回原值。
細心的已經(jīng)開始看注釋了。
官方注釋解釋了,如果參數(shù)是Integer.MIN_VALUE,返回的還是自身?,F(xiàn)象和描述相符,但是和代碼不符,按照代碼運行應該是-Integer.MIN_VALUE,負負得正。
玄機在哪呢?
沒錯,就是你想的那樣。
你答對了
越界了。
int類型占32位,數(shù)值范圍是[-2147483648, 2147483647]。-Integer.MIN_VALUE是2147483648,比Integer.MAX_VALUE的2147483647大,所以越界了。
在計算機系統(tǒng)中,數(shù)值一律用補碼來表示和存儲。使用補碼的主要優(yōu)點是可以將符號位和數(shù)值位統(tǒng)一處理,同時加法和減法也可以統(tǒng)一處理。
補碼系統(tǒng)中,正數(shù)的補碼就是其本身,負數(shù)的補碼是在其原碼的基礎上,符號位不變,其余各位取反,最后加1。
在補碼表示法中,Integer.MIN_VALUE的二進制表示是10000000000000000000000000000000,取反后的結(jié)果是01111111111111111111111111111111,再加一變成10000000000000000000000000000000,這正好是Integer.MIN_VALUE本身。
因此,Math.abs(Integer.MIN_VALUE)的結(jié)果仍然是Integer.MIN_VALUE,即-2147483648。
解決問題
如何避免這個問題呢?兩種方式:
- 擴展數(shù)值范圍,在調(diào)用Math.abs方法是,把hash轉(zhuǎn)為Long類型,這個時候就不會越界了;
- 判斷hash值是否是Integer.MIN_VALUE,如果是,直接返回0。
總結(jié)問題
是不是有朋友會想,JDK中居然有這么低級錯誤的API。不過,這次的錯誤和JDK中居然也有反模式接口常量的不一樣,這次是“抽象泄漏法則”的一種情況。
什么是抽象泄漏法則?
“抽象泄漏”是軟件開發(fā)時,本應隱藏實現(xiàn)細節(jié)的抽象化不可避免地暴露出底層細節(jié)與局限性。抽象泄露是棘手的問題,因為抽象化本來目的就是向用戶隱藏不必要公開的細節(jié)。
由于軟件開發(fā)與運行環(huán)境越來越復雜,開發(fā)者必須依賴于各種抽象。使得開發(fā)者專注于高層次的領域相關的知識與技能,以提高工作效率。
但是,抽象泄漏法則指出“可靠”軟件的開發(fā)者必須了解抽象之下的底層細節(jié)。否則一旦出了任何問題,根本不會知道是怎么回事,也不知道如何除錯或回復。
程序設計工具抽象掉某些東西,但和其他所有抽象機制一樣都有漏洞,而唯一能適當處理漏洞的方法,就是弄懂該抽象原理以及所隱藏的東西。
所以抽象機制雖然節(jié)省了工作的時間,不過學習的時間是省不掉的。
比如本次的問題,Math.abs函數(shù)本身是一個簡單的抽象,用于計算一個數(shù)的絕對值。然而,由于 Integer.MIN_VALUE和反碼的特殊性質(zhì),Math.abs(Integer.MIN_VALUE)仍然返回負數(shù),這暴露了底層整數(shù)表示的細節(jié)。
這種抽象的泄漏使得我們在使用Math.abs(hash) % list.size()計算索引值時遇到了意料之外的問題。為了了解其中的原因,我們得需要知道數(shù)值在計算機中的表示形式。
所以,工具雖然好用,碰到問題也得撓頭。
少年,做好成為高級程序員的準備了嗎?