你所不知道的 BigDecimal

本文首發於個人微信公衆號《andyqian》,期待你的關注~

前言

在Java中,我們通常使用 BigDecimal 類型來表示金額,特別是在金融,財務系統中,使用的特別多。例如:轉賬金額,手續費等等。今天就一起來認識下BigDecimal。

爲什麼是BigDecimal ?

在此之前,我們先來講講爲什麼要使用 BigDecimal ?而不是Float,Double類型?其實光從表現形式來看,Float,Double,BigDecimal 類型都能表示小數。其區別在於精確計算時,Float 與 Double 類型都會損失精度,當然了,BigDecimal 使用不正確時,也會損失精度。在金融系統中,金額計算是最基本的運算,精度的丟失是絕對不能容忍的。接下來,我們來看看下面的例子:

Float 類型:

public void testFloat(){
    float a = 1.1f;
    float b = 0.8f;
    System.out.println("a-b = "+(a-b));
    System.out.println("a+b = "+(a+b));
    System.out.println("a*b = "+(a*b));
    System.out.println("a/b = "+(a/b));
}

結果如下:

a-b = 0.3
a+b = 1.9000001
a*b = 0.88000005
a/b = 1.375

Double 類型:

public void testDouble(){
    double a = 1.1;
    double b = 0.8;
    System.out.println("a-b = "+(a-b));
    System.out.println("a+b = "+(a+b));
    System.out.println("a*b = "+(a*b));
    System.out.println("a/b = "+(a/b));
}

結果如下:

a-b = 0.30000000000000004
a+b = 1.9000000000000001
a*b = 0.8800000000000001
a/b = 1.375

BigDecmial 錯誤使用:

public void testBigDecimal(){
    BigDecimal a = new BigDecimal(1.1);
    BigDecimal b = new BigDecimal(0.8);
    System.out.println("a-b = "+(a.subtract(b)));
    System.out.println("a+b = "+(a.add(b)));
    System.out.println("a*b = "+(a.multiply(b)));
    System.out.println("a/b = "+(a.divide(b)));
}

結果如下:

a-b = 0.3000000000000000444089209850062616169452667236328125
a+b = 1.9000000000000001332267629550187848508358001708984375
a*b = 0.8800000000000001199040866595169103100567462588676208086428264139311483660321755451150238513946533203125

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

正確使用方法:

public void testBigDecimalNormal(){
    BigDecimal a = new BigDecimal("1.1");
    BigDecimal b = new BigDecimal("0.8");
    System.out.println("a-b = "+(a.subtract(b)));
    System.out.println("a+b = "+(a.add(b)));
    System.out.println("a*b = "+(a.multiply(b)));
    System.out.println("a/b = "+(a.divide(b)));
}

結果如下:

a-b = 0.3
a+b = 1.9
a*b = 0.88
a/b = 1.375

通過上面的例子,我們可以清晰的看出。除了正確使用BigDecimal類型外,其餘的在計算過程中,均損失精度。因此我們可以得出以下結論:

  1. 在需要精度計算數值時,不應該使用float,double 類型,進行計算。
  2. BigDecimal 應該使用 String 構造函數,禁止使用double構造函數。

使用細節

其實,在使用BigDecimal過程,也有許多需要注意的細節。

  1. 科學計數法問題
@Test
    public void testBigDecimalResult(){
        BigDecimal b = new BigDecimal("0.0000001");
        System.out.println(b.toString());
        System.out.println(b.toPlainString());
    }

執行結果:

1E-7
0.0000001

結論:當 BigDecimal的值 小於一定值時(測試時發現:小於等於0.0000001)時,則會被記爲科學計數法。可以使用 toPlainString()
方法顯示原來的值。

2. 去除多餘的 0

@Test
    public void testBigDecimalStripZeros(){
        BigDecimal b = new BigDecimal("0.000000100000000");
        System.out.println(b.stripTrailingZeros().toString());
        System.out.println(b.stripTrailingZeros().toPlainString());
    }

使用場景:去除多餘的0,當金額有小數位限制時,使用該方法能夠去除掉無效的0,從而達到自動修復無效參數的目的。

結論:stripTrailingZeros() 方法的本質是去除掉多餘的0,其返回數據類型是BigDecimal,同樣的在使用時需要注意科學技術法的問題。

3. 保留小數位

@Test
    public void testBigDecimalStripZeros(){
        BigDecimal d = new BigDecimal("1.2222");
        d.setScale(2);
        System.out.println(d.toPlainString());
    }

運行結果:

java.lang.ArithmeticException: Rounding necessary

原因:在setScale()方法中的roundingMode屬性設置爲了ROUND_UNNECESSARY,代碼如下:

public BigDecimal setScale(int newScale) {
        return setScale(newScale,);
    }

而在:

java.math.BigDecimal.commonNeedIncrement(BigDecimal.java:4179)

中ROUND_UNNECESSARY 類型恰恰會拋出異常。代碼顯示如下:

private static boolean commonNeedIncrement(int roundingMode, int qsign,
                                        int cmpFracHalf, boolean oddQuot) {
        switch(roundingMode) {
        case ROUND_UNNECESSARY:
            throw new ArithmeticException("Rounding necessary");

        case ROUND_UP: // Away from zero
            return true;
    ...

小結

通過上面的例子,現在我們已經知道了BigDecimal的一些使用細節。其實呀,這些都是血淋淋的教訓換來的經驗,每一個小細節對應的都是一個個事故,記憶猶新。這裏推薦大家都抽時間看看《Java開發手冊》,就能避免掉很多坑。


上面的問題,在《Java開發手冊》中同樣有寫到:

【強制】爲了防止精度損失,禁止使用構造方法BigDecimal(double)的方式把double值轉化爲BigDecimal對象。

說明:BigDecimal(double) 存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。如:BigDecimal g = new BigDecimal(0.1f); 實際的存儲值爲:0.10000000149

正例:優先推薦入參爲String 的構造函數,或使用BigDecimal的valueOf方法。

 

相關閱讀:

軟件之路 之 項目外包

ThreadPoolExecutor 原理解析

Java線程池ThreadPoolExecutor

再談Java 生產神器 BTrace

 

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