我去,臉皮厚啊,你竟然使用==比較浮點數?
先看再點贊,給自己一點思考的時間,思考過後請毫不猶豫微信搜尋【沉默王二】,關注這個長髮飄飄卻靠才華苟且的程式設計師。
本文 GitHub github.com/itwanger 已收錄,裡面還有技術大佬整理的面試題,以及二哥的系列文章。
老讀者都知道了,我在九朝古都洛陽的一家小作坊式的公司工作,身兼數職,談業務、敲程式碼的同時帶兩個新人,其中一個就是大家熟知的小王,經常犯錯,被我寫到文章裡。
不過,小王的心態一直很不錯,他不覺得被我批評有什麼丟人的,反而每次讀完我的文章後覺得自己又升級了。因此,我覺得小王大有前途,再這麼幹個一兩年,老闆要是覺得我的價效比低了,沒準就把我辭退留下小王了。一想到這,我竟然枯燥一笑了。
那天,我閒來無聊,就準備偷偷 review 一下小王的程式碼,看能不能雞蛋裡挑點骨頭,沒想到,還真的被我挑到了。
double d1 = .0;
for (int i = 1; i <= 11; i++) {
d1 += .1;
}
double d2 = .1 * 11;
System.out.println(d1 == d2);
小王這段程式碼蠻炫技的,其實,尤其是 .0
.1
的寫法,我平常都老實巴交的寫成 0.0
、0.1
,從來沒想著要把小數點前面的 0 省略。
按照正常的邏輯來看,d1 在經過 11 次迴圈加 .1
後的結果應該是 1.1
,d2 通過 .1
乘以 11 後的結果也應該是 1.1
,最後打印出來的結果應該是 true,對吧?小王應該也是這麼期待的,我覺得。
但我當時硬是沒忍住我的暴脾氣,破口大罵:“我擦,小王,你竟然敢用 == 比較浮點數,這不是找刺激嗎?”
如果有讀者也覺得輸出結果是 true 的話,可以把上面這段程式碼在本地執行一下,輸出的結果一定會出乎你的意料。
false
對,false,我沒騙你。如何正確地比較浮點數(單精度的 float 和雙精度的 double),不單單是 Java 特定的問題,很多程式語言的初學者也會遇到同樣的問題。在計算機的記憶體中,儲存浮點數時使用的是 IEEE 754 標準,就會有精度的問題,至於實際上的儲存轉換過程,這篇文章不做過多的探討。
(主要是我太菜了,探討的過程很枯燥,一點都不有趣,嚴謹地理論推導就交給那些真正的技術大佬們吧,我就不獻醜了。)
同學們只需要知道,儲存和轉換的過程中浮點數容易引起一些較小的舍入誤差,正是這個原因,導致在比較浮點數的時候,不能使用“==”操作符——要求嚴格意義上的完全相等。
再來看一下小王的程式碼,我們把 d1 和 d2 打印出來,看看它們的值到底是什麼。
d1:1.0999999999999999
d2:1.1
怪不得“==”的時候輸出 false,原來 d1 的值有一些誤差,並不是我們預期的 1.1。既然“==”不能用來比較浮點數,那麼小王就得捱罵,這邏輯講得通吧?
那這個問題該怎麼解決呢?
對於浮點數的儲存和轉化問題,我表示無能為力,這是實在話,計算機的底層問題,駕馭不了。但是,可以通過一些折中的辦法,比如說允許兩個值之間有點誤差(指定一個閾值),小到 0.000000…..1,具體多少個 0 懶得數了,反正特別小,那麼我們就認為兩個浮點數是相等的。
第一種方案就是使用 Math.abs()
方法來計算兩個浮點數之間的差異,如果這個差異在閾值範圍之內,我們就認為兩個浮點數是相等。
final double THRESHOLD = .0001;
double d1 = .0;
for (int i = 1; i <= 11; i++) {
d1 += .1;
}
double d2 = .1 * 11;
if(Math.abs(d1-d2) < THRESHOLD) {
System.out.println("d1 和 d2 相等");
} else {
System.out.println("d1 和 d2 不等");
}
Math.abs()
方法用來返回 double 的絕對值,如果 double 小於 0,則返回 double 的正值,否則返回 double。也就是說,abs()
後的結果絕對大於 0,如果結果小於閾值(THRESHOLD),我們就認為 d1 和 d2 相等。
第二種解決方案就是使用 BigDecimal 類,可以指定要舍入的模式和精度,這樣就可以解決舍入的誤差。
可以使用 BigDecimal 類的 compareTo()
方法對兩個數進行比較,該方法將會忽略小數點後的位數,怎麼理解這句話呢?比如說 2.0 和 2.00 的位數不同,但它倆的值是相等的。
如果 a 小於 b,則該方法返回 -1,如果相等,則返回 0,否則返回 -1。
注意,千萬不要使用 equals()
方法對兩個 BigDecimal 物件進行比較,這是因為 equals()
方法會考慮位數,如果位數不同,則會返回 false,儘管數學值是相等的。
BigDecimal a = new BigDecimal("2.00");
BigDecimal b = new BigDecimal("2.0");
System.out.println(a.equals(b));
System.out.println(a.compareTo(b) == 0);
a.equals(b)
的結果就為 false,因為 2.00 和 2.0 小數點後的位數不同,但 a.compareTo(b) == 0
的結果就為 true,因為 2.00 和 2.0 在數學層面的值的確是相等的。
compareTo()
方法比較的過程非常嚴謹,感興趣的同學可以檢視一下原始碼,其中位數不同的時候,會執行以下方法進行比較。
private int compareMagnitude(BigDecimal val) {
// Match scales, avoid unnecessary inflation
long ys = val.intCompact;
long xs = this.intCompact;
if (xs == 0)
return (ys == 0) ? 0 : -1;
if (ys == 0)
return 1;
long sdiff = (long)this.scale - val.scale;
if (sdiff != 0) {
// Avoid matching scales if the (adjusted) exponents differ
long xae = (long)this.precision() - this.scale; // [-1]
long yae = (long)val.precision() - val.scale; // [-1]
if (xae < yae)
return -1;
if (xae > yae)
return 1;
if (sdiff < 0) {
// The cases sdiff <= Integer.MIN_VALUE intentionally fall through.
if ( sdiff > Integer.MIN_VALUE &&
(xs == INFLATED ||
(xs = longMultiplyPowerTen(xs, (int)-sdiff)) == INFLATED) &&
ys == INFLATED) {
BigInteger rb = bigMultiplyPowerTen((int)-sdiff);
return rb.compareMagnitude(val.intVal);
}
} else { // sdiff > 0
// The cases sdiff > Integer.MAX_VALUE intentionally fall through.
if ( sdiff <= Integer.MAX_VALUE &&
(ys == INFLATED ||
(ys = longMultiplyPowerTen(ys, (int)sdiff)) == INFLATED) &&
xs == INFLATED) {
BigInteger rb = val.bigMultiplyPowerTen((int)sdiff);
return this.intVal.compareMagnitude(rb);
}
}
}
if (xs != INFLATED)
return (ys != INFLATED) ? longCompareMagnitude(xs, ys) : -1;
else if (ys != INFLATED)
return 1;
else
return this.intVal.compareMagnitude(val.intVal);
}
好了,現在讓我們使用 BigDecimal 來解決精度問題吧。
BigDecimal d1 = new BigDecimal("0.0");
BigDecimal pointOne = new BigDecimal("0.1");
for (int i = 1; i <= 11; i++) {
d1 = d1.add(pointOne);
}
BigDecimal d2 = new BigDecimal("0.1");
BigDecimal eleven = new BigDecimal("11");
d2 = d2.multiply(eleven);
System.out.println("d1 = " + d1);
System.out.println("d2 = " + d2);
System.out.println(d1.compareTo(d2));
程式輸出的結果如下:
d1 = 1.1
d2 = 1.1
0
d1 和 d2 都為 1.1,所以 compareTo()
的結果就為 0,表示兩個值是相等的。
總結一下,在遇到浮點數的時候,千萬不要使用“==”操作符來進行比較,因為有精度問題。要麼使用閾值來忽略舍入的問題,要麼使用 BigDecimal 來替代 double 或者 float。
等會我就把這篇文章發給小王看看,同學們順手點個贊