我去,臉皮厚啊,你竟然使用==比較浮點數?
老讀者都知道了,我在九朝古都洛陽的一家小作坊式的公司工作,身兼數職,談業務、敲程式碼的同時帶兩個新人,其中一個就是大家熟知的小王,經常犯錯,被我寫到文章裡。
不過,小王的心態一直很不錯,他不覺得被我批評有什麼丟人的,反而每次讀完我的文章後覺得自己又升級了。因此,我覺得小王大有前途,再這麼幹個一兩年,老闆要是覺得我的價效比低了,沒準就把我辭退留下小王了。一想到這,我竟然枯燥一笑了。
那天,我閒來無聊,就準備偷偷 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。
等會我就把這篇文章發給小王看看,同學們順手點個贊,讓小王不再感到那麼孤單寂寞和冷。
相關文章
- 浮點數的比較
- 【Java】浮點數相等性比較Java
- js精確比較浮點數大小JS
- Java如何正確比較浮點數Java
- C++ - 比較兩個浮點數大小C++
- PostgreSQL 原始碼解讀(196)- 浮點數比較SQL原始碼
- 我去,你竟然還不會用 synchronizedsynchronized
- 我去,你竟然還不會用 Java final 關鍵字Java
- 技術大佬:我去,你竟然還不會用 this 關鍵字
- 浮點數
- 浮點數之間的比較,基本運算這些究竟是怎麼實現的
- 浮點數的這些坑,你未必知道
- 昨晚12點,女朋友突然問我:你會RabbitMQ嗎?我竟然愣住了。MQ
- 數字比較
- 浮點數的理解
- 浮點數小知識點
- 你真的懂 == 比較嗎
- 浮點數加減法
- 淺談浮點數(一)
- IEEE浮點數表示法
- 轉換成浮點數
- python處理浮點數Python
- 你不知道的JavaScript--Item2 浮點數精度JavaScript
- 大數相乘(浮點數)實現
- Linux指令碼中帶有小數點的數值比較大小Linux指令碼
- Python 解惑:整數比較Python
- JS變數比較陷阱JS變數
- 部門要我組織培訓,培訓點啥比較好
- 補碼、反碼、浮點數
- Java中浮點數的坑Java
- JS中如何理解浮點數?JS
- iOS浮點數精度問題iOS
- JavaScript浮點數保留兩位小數JavaScript
- 我去嗶哩嗶哩總部面試Android開發,竟然...面試Android
- ES6的Symbol竟然那麼強大,面試中的加分點啊Symbol面試
- 深入理解浮點數的表示
- IEEE754浮點數表示法
- 匹配浮點數正規表示式