我去,臉皮厚啊,你竟然使用==比較浮點數?

yunduo1發表於2020-10-09

我去,臉皮厚啊,你竟然使用==比較浮點數?

 

老讀者都知道了,我在九朝古都洛陽的一家小作坊式的公司工作,身兼數職,談業務、敲程式碼的同時帶兩個新人,其中一個就是大家熟知的小王,經常犯錯,被我寫到文章裡。

不過,小王的心態一直很不錯,他不覺得被我批評有什麼丟人的,反而每次讀完我的文章後覺得自己又升級了。因此,我覺得小王大有前途,再這麼幹個一兩年,老闆要是覺得我的價效比低了,沒準就把我辭退留下小王了。一想到這,我竟然枯燥一笑了。

我去,臉皮厚啊,你竟然使用==比較浮點數?

 

那天,我閒來無聊,就準備偷偷 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。

等會我就把這篇文章發給小王看看,同學們順手點個贊,讓小王不再感到那麼孤單寂寞和冷。

相關文章