JAVA 浮點類型的坑

背景

今天在項目裏踩到了個坑,就是浮點類型的四則運算後,發現結果不是預期的。

上代碼:

public class DateTypeApplication {
    public static void main(String[] args) {
        System.out.println(2.43 +0.031);
    }
}

預期結果:2.461
實際結果:2.4610000000000003

原因分析

2.43 +0.031 兩個數字都是double類型的,執行 “ + ”操作的時候。系統處理的流程如下
1:把 2.43 和0.031都轉換成二制數字
2:然後執行 “ +” 操作。
3:再結果轉成 十進制數。

衆所周知,10進度的小數部分轉成二進度是有精度丟失風險的。就好像 1/3 的 計算一樣,結果有可能會是個無窮數,最後只能截斷,然後導致丟失精度。
那麼在第1步的時候就已經把精度丟失了,後面就會一直錯下去。。貌似解釋通了。

解決方案

Java提供了 BigDecimal類型爲解決這個問題。

public class DateTypeApplication {
    public static void main(String[] args) {
        System.out.println(BigDecimal.valueOf(2.43).add(BigDecimal.valueOf(0.031)));
    }
}

結果
2.461

疑問與吐槽

因爲目前的使用習慣是十進制。而JAVA卻反其道而行,顯示用的是十進制,計算卻用的是二進制。
另外,雖然JAVA也提供瞭解決的方案BigDecimal,但我想說這是屎一樣的設計,屎一般的解決方案。。
站在開發者的角度,你告訴我2.43 +0.031 直接用是錯的,應該用: BigDecimal.valueOf(2.43).add(BigDecimal.valueOf(0.031))。
總結差異包括以下:
1:思維方式不夠直接。當然寫多了還是可以習慣。我每寫一次我就想吐槽一次。
2:代碼量多了,又有數據類型的轉換。
3:代碼的可讀性不好。
最後,我還是想吐槽,幹麼不直接解決 double的問題,而是重新搞一套?

原因與發展過程

做過了一些語言的橫向對比,包括C++,javascript, C#都會有類似的問題,其中C#的double類型微軟做過優化,精度的問題比較少,但重複多次的試驗之後,也照樣的問題。
既然都有問題,那麼這應該是個行業準備,是的。遵循的是IEEE 754標準。

IEEE 754標準

什麼是IEEE 754標準?
最權威的解釋是IEEE754標準本身ANSI/IEEE Std 754-1985《IEEE Standard for Binary Floating-Point Arithmetic》,網上有PDF格式的文件,一下,下載即可。標準文本是英文的,總共才23頁,有耐心的話可以仔細閱讀。IEEE754

IEEE 754 規定:

a) 兩種基本浮點格式:單精度和雙精度。
IEEE單精度格式具有24位有效數字,並總共佔用32 位。IEEE雙精度格式具有53位有效數字精度,並總共佔用64位。
說明:基本浮點格式是固定格式,相對應的十進制有效數字分別爲7位和17位。基本浮點格式對應的C/C++類型爲float和double。
b) 兩種擴展浮點格式:單精度擴展和雙精度擴展。
此標準並未規定擴展格式的精度和大小,但它指定了最小精度和大小。例如,IEEE 雙精度擴展格式必須至少具有64位有效數字,並總共佔用至少79 位。
說明:雖然IEEE 754標準沒有規定具體格式,但是實現者可以選擇符合該規定的格式,一旦實現,則爲固定格式。例如:x86 FPU是80位擴展精度,而Intel安騰FPU是82位擴展精度,都符合IEEE 754標準的規定。C/C++對於擴展雙精度的相應類型是long double,但是,Microsoft Visual C++ 6.0版本以上的編譯器都不支持該類型,long double和double一樣,都是64位基本雙精度,只能用其它C/C++編譯器或彙編語言。
c) 浮點運算的準確度要求:加、減、乘、除、平方根、餘數、將浮點格式的數舍入爲整數值、在不同浮點格式之間轉換、在浮點和整數格式之間轉換以及比較。
求餘和比較運算必須精確無誤。其他的每種運算必須向其目標提供精確的結果,除非沒有此類結果,或者該結果不滿足目標格式。對於後一種情況,運算必須按照下面介紹的規定舍入模式的規則對精確結果進行最低限度的修改,並將經過此類修改的結果提供給運算的目標。
說明:IEEE 754沒有規定基本算術運算(+、-、×、/ 等)的結果必須精確無誤,因爲對於IEEE 754的二進制浮點數格式,由於浮點格式長度固定,基本運算的結果幾乎不可能精確無誤。這裏用三位精度的十進制加法來說明:
例1:a = 3.51,b = 0.234,求a+b = ?
a與b都是三位有效數字,但是,a+b的精確結果爲3.744,是四位有效數字,對於該浮點格式只有三位精度,a+b的結果無法精確表示,只能近似表示,具體運算結果取決於舍入模式(見舍入模式的說明)。同理,由於浮點格式固定,對於其他基本運算,結果也幾乎無法精確表示。

還是有點疑惑

標準說了說了那麼多,到底說十進度浮點數轉二進制丟失精度是必然的,而丟失精度又是依有據合理的。但這麼說我又不服了,爲什麼要用這個標準。。不是可以做到準確嗎?幹麼又不用替換掉不準確的。我又疑惑了。
於是我又尋找相關資料。

性能

二進度是硬件級別支持的,而十進制則不支持,用軟件實現的十進制浮點計算比硬件實現的二進制浮點計算要慢100-1000倍。
以下我做個實驗。

import org.springframework.util.StopWatch;

import java.math.BigDecimal;

public class DateTypePerformanceTestApplication {

    public static void main(String[] args) {

        StopWatch watch =new StopWatch();
        watch.start();
        double d1=2.6;

        for(int i=0;i<10000000;i++)
        {
            d1+=0.01;
        }
        watch.stop();
        System.out.println("double計算花耗時間:"+watch.getTotalTimeMillis() +",最終值:"+d1);
        
        watch =new StopWatch();
        watch.start();
        BigDecimal  d2= BigDecimal.valueOf(2.6) ;
        for(int i=0;i<10000000;i++)
        {
            d2.add( new BigDecimal(0.1));
        }
        watch.stop();
        System.out.println("BigDecimal計算花耗時間:"+watch.getTotalTimeMillis()+",最終值:"+d2);

    }
}

最終結果

double計算花耗時間:12,最終值:100002.59998630833
BigDecimal計算花耗時間:3461,最終值:2.6

實際上測試的性能是二制度比十進制超過300多倍。多次累計操作的結果也相差甚遠。

寫在最後

1:性能與精準不能同理兼顧。
2:開發人員需要根據不同應用場所去使用合適的方法。
1)金融,財務類的還是需要使用BigDecimal。如果大批量的統計儘量放到數據庫去處理。
2)頁面UI計算類的,可以直接使用double。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章