我去,臉皮厚啊,你竟然使用==比較浮點(diǎn)數(shù)?

先看再點(diǎn)贊,給自己一點(diǎn)思考的時(shí)間,微信搜索【沉默王二】關(guān)注這個(gè)靠才華茍且的程序員。
本文 GitHub github.com/itwanger 已收錄,里面還有一線大廠整理的面試題,以及我的系列文章。

老讀者都知道了,我在九朝古都洛陽(yáng)的一家小作坊式的公司工作,身兼數(shù)職,談業(yè)務(wù)、敲代碼的同時(shí)帶兩個(gè)新人,其中一個(gè)就是大家熟知的小王,經(jīng)常犯錯(cuò),被我寫(xiě)到文章里。

不過(guò),小王的心態(tài)一直很不錯(cuò),他不覺(jué)得被我批評(píng)有什么丟人的,反而每次讀完我的文章后覺(jué)得自己又升級(jí)了。因此,我覺(jué)得小王大有前途,再這么干個(gè)一兩年,老板要是覺(jué)得我的性?xún)r(jià)比低了,沒(méi)準(zhǔn)就把我辭退留下小王了。一想到這,我竟然枯燥一笑了。

那天,我閑來(lái)無(wú)聊,就準(zhǔn)備偷偷 review 一下小王的代碼,看能不能雞蛋里挑點(diǎn)骨頭,沒(méi)想到,還真的被我挑到了。

double d1 = .0;
for (int i = 1; i <= 11; i++) {
    d1 += .1;
}

double d2 = .1 * 11;

System.out.println(d1 == d2);

小王這段代碼蠻炫技的,其實(shí),尤其是 .0.1 的寫(xiě)法,我平常都老實(shí)巴交的寫(xiě)成 0.0、0.1,從來(lái)沒(méi)想著要把小數(shù)點(diǎn)前面的 0 省略。

按照正常的邏輯來(lái)看,d1 在經(jīng)過(guò) 11 次循環(huán)加 .1 后的結(jié)果應(yīng)該是 1.1,d2 通過(guò) .1 乘以 11 后的結(jié)果也應(yīng)該是 1.1,最后打印出來(lái)的結(jié)果應(yīng)該是 true,對(duì)吧?小王應(yīng)該也是這么期待的,我覺(jué)得。

但我當(dāng)時(shí)硬是沒(méi)忍住我的暴脾氣,破口大罵:“我擦,小王,你竟然敢用 == 比較浮點(diǎn)數(shù),這不是找刺激嗎?”

如果有讀者也覺(jué)得輸出結(jié)果是 true 的話(huà),可以把上面這段代碼在本地運(yùn)行一下,輸出的結(jié)果一定會(huì)出乎你的意料。

false

對(duì),false,我沒(méi)騙你。如何正確地比較浮點(diǎn)數(shù)(單精度的 float 和雙精度的 double),不單單是 Java 特定的問(wèn)題,很多編程語(yǔ)言的初學(xué)者也會(huì)遇到同樣的問(wèn)題。在計(jì)算機(jī)的內(nèi)存中,存儲(chǔ)浮點(diǎn)數(shù)時(shí)使用的是 IEEE 754 標(biāo)準(zhǔn),就會(huì)有精度的問(wèn)題,至于實(shí)際上的存儲(chǔ)轉(zhuǎn)換過(guò)程,這篇文章不做過(guò)多的探討。

(主要是我太菜了,探討的過(guò)程很枯燥,一點(diǎn)都不有趣,嚴(yán)謹(jǐn)?shù)乩碚撏茖?dǎo)就交給那些真正的技術(shù)大佬們吧,我就不獻(xiàn)丑了。)

同學(xué)們只需要知道,存儲(chǔ)和轉(zhuǎn)換的過(guò)程中浮點(diǎn)數(shù)容易引起一些較小的舍入誤差,正是這個(gè)原因,導(dǎo)致在比較浮點(diǎn)數(shù)的時(shí)候,不能使用“==”操作符——要求嚴(yán)格意義上的完全相等。

再來(lái)看一下小王的代碼,我們把 d1 和 d2 打印出來(lái),看看它們的值到底是什么。

d1:1.0999999999999999
d2:1.1

怪不得“==”的時(shí)候輸出 false,原來(lái) d1 的值有一些誤差,并不是我們預(yù)期的 1.1。既然“==”不能用來(lái)比較浮點(diǎn)數(shù),那么小王就得挨罵,這邏輯講得通吧?

那這個(gè)問(wèn)題該怎么解決呢?

對(duì)于浮點(diǎn)數(shù)的存儲(chǔ)和轉(zhuǎn)化問(wèn)題,我表示無(wú)能為力,這是實(shí)在話(huà),計(jì)算機(jī)的底層問(wèn)題,駕馭不了。但是,可以通過(guò)一些折中的辦法,比如說(shuō)允許兩個(gè)值之間有點(diǎn)誤差(指定一個(gè)閾值),小到 0.000000.....1,具體多少個(gè) 0 懶得數(shù)了,反正特別小,那么我們就認(rèn)為兩個(gè)浮點(diǎn)數(shù)是相等的。

第一種方案就是使用 Math.abs() 方法來(lái)計(jì)算兩個(gè)浮點(diǎn)數(shù)之間的差異,如果這個(gè)差異在閾值范圍之內(nèi),我們就認(rèn)為兩個(gè)浮點(diǎn)數(shù)是相等。

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() 方法用來(lái)返回 double 的絕對(duì)值,如果 double 小于 0,則返回 double 的正值,否則返回 double。也就是說(shuō),abs() 后的結(jié)果絕對(duì)大于 0,如果結(jié)果小于閾值(THRESHOLD),我們就認(rèn)為 d1 和 d2 相等。

第二種解決方案就是使用 BigDecimal 類(lèi),可以指定要舍入的模式和精度,這樣就可以解決舍入的誤差。

可以使用 BigDecimal 類(lèi)的 compareTo() 方法對(duì)兩個(gè)數(shù)進(jìn)行比較,該方法將會(huì)忽略小數(shù)點(diǎn)后的位數(shù),怎么理解這句話(huà)呢?比如說(shuō) 2.0 和 2.00 的位數(shù)不同,但它倆的值是相等的。

如果 a 小于 b,則該方法返回 -1,如果相等,則返回 0,否則返回 -1。

注意,千萬(wàn)不要使用 equals() 方法對(duì)兩個(gè) BigDecimal 對(duì)象進(jìn)行比較,這是因?yàn)?equals() 方法會(huì)考慮位數(shù),如果位數(shù)不同,則會(huì)返回 false,盡管數(shù)學(xué)值是相等的。

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) 的結(jié)果就為 false,因?yàn)?2.00 和 2.0 小數(shù)點(diǎn)后的位數(shù)不同,但 a.compareTo(b) == 0 的結(jié)果就為 true,因?yàn)?2.00 和 2.0 在數(shù)學(xué)層面的值的確是相等的。

compareTo() 方法比較的過(guò)程非常嚴(yán)謹(jǐn),感興趣的同學(xué)可以查看一下源碼,其中位數(shù)不同的時(shí)候,會(huì)執(zhí)行以下方法進(jìn)行比較。

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);
}

好了,現(xiàn)在讓我們使用 BigDecimal 來(lái)解決精度問(wèn)題吧。

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));

程序輸出的結(jié)果如下:

d1 = 1.1
d2 = 1.1
0

d1 和 d2 都為 1.1,所以 compareTo() 的結(jié)果就為 0,表示兩個(gè)值是相等的。

總結(jié)一下,在遇到浮點(diǎn)數(shù)的時(shí)候,千萬(wàn)不要使用“==”操作符來(lái)進(jìn)行比較,因?yàn)橛芯葐?wèn)題。要么使用閾值來(lái)忽略舍入的問(wèn)題,要么使用 BigDecimal 來(lái)替代 double 或者 float。

等會(huì)我就把這篇文章發(fā)給小王看看,同學(xué)們順手點(diǎn)個(gè)贊??,讓小王不再感到那么孤單寂寞和冷。


我是沉默王二,一枚有顏值卻靠才華茍且的程序員。關(guān)注即可提升學(xué)習(xí)效率,別忘了三連啊,點(diǎn)贊、收藏、留言,我不挑,奧利給

注:如果文章有任何問(wèn)題,歡迎毫不留情地指正。

很多讀者都同情我說(shuō),“二哥,你像母豬似的日更原創(chuàng)累不累?”我只能說(shuō)寫(xiě)作不易,且行且珍惜啊,關(guān)鍵是我真的喜歡寫(xiě)作。最后,歡迎微信搜索「沉默王二」第一時(shí)間閱讀,回復(fù)「簡(jiǎn)歷」更有阿里大佬的簡(jiǎn)歷模板,本文 GitHub github.com/itwanger 已收錄,歡迎 star。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容