不掌握 BigDecimal 的四大坑你敢用嗎?
BigDecimal 是 Java 中的一個(gè)類,這個(gè)相信大家都是知道的。它的作用就是可以表示任意精度的十進(jìn)制數(shù),BigDecimal 提供了精確的數(shù)字運(yùn)算,適用于需要高精度計(jì)算的場(chǎng)景,例如金融、貨幣或者稅收等涉及到金錢的地方。
與 double 和 float 不同的是,BigDecimal 對(duì)象在計(jì)算的過程中不會(huì)丟失精度,那么下面我們就來看下第一個(gè)坑,浮點(diǎn)精度的坑。

一、浮點(diǎn)精度的坑
我們先來看一個(gè)例子:
    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("0.1");
        BigDecimal num2 = new BigDecimal("0.10");
        // false
        System.out.println(num1.equals(num2));
        // 0
        System.out.println(num1.compareTo(num2));
    }compareTo 方法比較中,a.compareTo(b)
返回:
- -1: a小于b
 - 0: a等于b
 - 1: a大于b。
 
在上方的代碼中,我們使用 new BigDecimal 的形式 new 了兩個(gè) BigDecimal 對(duì)象,分別是 0.1 和0.10。
我們分別使用了 equals 與 compareTo 進(jìn)行比較,當(dāng)使用 equals 進(jìn)行比較時(shí),返回了 false,這是因?yàn)?nbsp;equals 不僅比較了值是否相等,還比較了精度是否相等,源碼中是這樣寫的:
 public boolean equals(Object x) {
        if (!(x instanceof BigDecimal))
            return false;
        BigDecimal xDec = (BigDecimal) x;
        if (x == this)
            return true;
        if (scale != xDec.scale)
            return false;
        long s = this.intCompact;
        long xs = xDec.intCompact;
        if (s != INFLATED) {
            if (xs == INFLATED)
                xs = compactValFor(xDec.intVal);
            return xs == s;
        } else if (xs != INFLATED)
            return xs == compactValFor(this.intVal);
        return this.inflated().equals(xDec.inflated());
    }所以在使用 equals 進(jìn)行比較兩個(gè) BigDecimal 的大小時(shí),一定要注意這一點(diǎn)了。
簡(jiǎn)單概括一下,如果比較兩個(gè) BigDecimal 對(duì)象的大小,那就使用 compareTo 方法;如果嚴(yán)格比較精度的大小,那就使用 equals 方法進(jìn)行比較。
上面我們知道了如何比較兩個(gè) BigDecimal 對(duì)象的大小,equals 比較的還有他們的精度,那么精度又是如何設(shè)置的呢,這塊有沒有坑呢?
二、設(shè)置精度的坑
有的同學(xué)可能會(huì)說了,設(shè)置精度還有啥坑啊,設(shè)置了精度就好了嗎,哎對(duì),就是這個(gè)意思,在做 BigDecimal 對(duì)象計(jì)算的時(shí)候,一定要設(shè)置精度。相反,有的同學(xué)就不喜歡設(shè)置精度,那么這 BUG 不就來了嗎。
來看一個(gè)例子:
    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("1");
        BigDecimal num2 = new BigDecimal("3");
        BigDecimal result = num1.divide(num2); // 默認(rèn)舍入模式為 UNNECESSARY,會(huì)拋出 ArithmeticException
    }上述的代碼在執(zhí)行結(jié)束之后會(huì)報(bào)錯(cuò) ArithmeticException ,這是因?yàn)槟J(rèn)舍入模式為 UNNECESSARY,所以會(huì)拋出 ArithmeticException。
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.要解決這個(gè)異常也很容易,只需要加上精度即可。
    public static void main(String[] args) {
        BigDecimal num1 = new BigDecimal("1");
        BigDecimal num2 = new BigDecimal("3");
        BigDecimal result = num1.divide(num2, 2,RoundingMode.HALF_UP);
        // 輸出:0.33
        System.out.println(result);
    }那么出現(xiàn)這個(gè)異常的原因是什么你考慮過嗎?為什么加了精度就不報(bào)錯(cuò)了呢?
這個(gè)異常在源碼中也有說明:

大概意思就是如果在做 divide 運(yùn)算時(shí),如果商是一個(gè)無限小數(shù),而操作的結(jié)果是一個(gè)精確的數(shù)字,那么就會(huì)拋出該異常。

不知道大家注意到一點(diǎn)沒有,就是上面做除法運(yùn)算的時(shí)候,也就是 BigDecimal result = num1.divide(num2, 2,RoundingMode.HALF_UP); 這行代碼的位置,使用了一個(gè)新的變量 result 來接收結(jié)果值,因?yàn)?nbsp;BigDecimal 是不可變的,因此每次進(jìn)行運(yùn)算都會(huì)創(chuàng)建一個(gè)新的 BigDecimal 對(duì)象,所以這一點(diǎn)也是需要注意的,創(chuàng)建的多了可能會(huì)產(chǎn)生大量的垃圾對(duì)象。
講完了精度與運(yùn)算,那么你初始化的方式對(duì)嗎?
三、初始化的坑
先來看代碼:
BigDecimal num = new BigDecimal(0.1); // 使用雙精度浮點(diǎn)數(shù)構(gòu)造
System.out.println(num); // 輸出: 0.1000000000000000055511151231257827021181583404541015625
BigDecimal num2 = new BigDecimal("0.1"); // 使用字符串構(gòu)造
System.out.println(num2); // 輸出: 0.1在使用 new BigDecimal 構(gòu)造器進(jìn)行初始化的時(shí)候,如果有初始值,最好使用字符串的構(gòu)造方法進(jìn)行初始化。

在使用 double 的構(gòu)造器進(jìn)行新建時(shí),本身傳入的 0.1 就是浮點(diǎn)類型了,為了不丟失精度,在使用 new BigDecimal 新建時(shí)就把這個(gè)近似值完整的保留下來了。
或者就是 另外一種初始化方式 BigDecimal.valueOf(0.1);,通過看源碼可以發(fā)現(xiàn),在 valueOf 的內(nèi)部,將 Double 類型直接轉(zhuǎn)為了字符串了,因此也就不會(huì)存在精度丟失的問題了。
對(duì)于使用 new BigDecimal(0.1) 構(gòu)造時(shí),源碼中也已經(jīng)說明了這個(gè)問題。

大體意思就是生成的 BigDecimal 對(duì)象不是我們想要的 0.1,推薦使用 String 類型的構(gòu)造方法。
上面我們已經(jīng)學(xué)會(huì)了如何初始化,如何運(yùn)算,下一步就是如何用了,例如轉(zhuǎn)字符串,很多同學(xué)可能會(huì)說,轉(zhuǎn)字符串 toString() 不就好了,如果你也這樣想,那你單純了弟弟。
四、轉(zhuǎn)字符串的坑
還是先看一段代碼:
    public static void main(String[] args) {
        BigDecimal a = BigDecimal.valueOf(89382389312389594.33822312317952678768725);
        System.out.println(a.toString()); // 輸出:8.93823893123896E+16
        String str = a.setScale(2, RoundingMode.HALF_UP).toString();
        System.out.println(str); // 輸出: 89382389312389600.00
    }上面代碼中是一個(gè)非常大的數(shù),我想把他轉(zhuǎn)為字符串,可是在使用 toString() 方法時(shí),打印出來的卻是科學(xué)計(jì)數(shù)法。
所以如果想使用 toString() 方法進(jìn)行轉(zhuǎn)字符串時(shí),可以使用設(shè)置精度的方法,但是結(jié)果還是與我們的預(yù)期有所差別,我們想要的是一模一樣的打印出來呢?
那么 toPlainString 就上場(chǎng)了,這個(gè)方法返回一個(gè)字符串的表示形式,包含所有的有效數(shù)字。
代碼修改如下:
    public static void main(String[] args) {
        BigDecimal a = BigDecimal.valueOf(89382389312389594.99933822312317952678768725);
        System.out.println(a.toPlainString());
    }修改之后就可以了嗎,不可以,忘了上面說的嗎,使用 String 的構(gòu)造函數(shù)吧兄弟,double 類型的構(gòu)造函數(shù)會(huì)丟失精度的。
最終代碼如下:
    public static void main(String[] args) {
        BigDecimal a = new BigDecimal("89382389312389594.99933822312317952678768725");
        System.out.println(a.toPlainString());
    }除了上述兩種轉(zhuǎn)字符串的方法外,還有一種,就是 toEngineeringString,這個(gè)方法也是返回一個(gè)字符串,包含有效數(shù)字,但是它會(huì)使用工程計(jì)數(shù)法,科學(xué)計(jì)數(shù)法的一種變體,它使用數(shù)字的倍數(shù)來表示值,使得指數(shù)是 3 的倍數(shù)。例如,1000會(huì)顯示為"1E3",而不是"1E+3"。
所以總結(jié)就是:
- toString:返回有效數(shù)字,必要的時(shí)候使用科學(xué)計(jì)數(shù)法。
 - toPlainString: 不實(shí)用任何科學(xué)計(jì)數(shù)法。
 - toEngineeringString:必要的時(shí)候使用工程計(jì)數(shù)法。
 
五、總結(jié)
本文從精度的比較、除法運(yùn)算中是否設(shè)置精度、對(duì)象初始化到轉(zhuǎn)字符串,四個(gè)角度來把 BigDecimal 的坑盡可能清晰的描述出來,以及基于這些坑得到的優(yōu)秀實(shí)踐。
有些場(chǎng)景下推薦使用 BigDecimal ,但是能不用還是不用,比 double 、float 多出來的性能損失得是你能接受的。如果非得用,那上面這幾個(gè)坑一定要規(guī)避。















 
 
 









 
 
 
 