java開發(fā)使用BigDecimal避坑四則
引言
在使用BigDecimal時,有4種使用場景下的坑,你一定要了解一下,如果使用不當(dāng),必定很慘。掌握這些案例,當(dāng)別人寫出有坑的代碼,你也能夠一眼識別出來,大牛就是這么練成的。
第一:浮點類型的坑
在學(xué)習(xí)了解BigDecimal的坑之前,先來說一個老生常談的問題:如果使用Float、Double等浮點類型進(jìn)行計算時,有可能得到的是一個近似值,而不是精確的值。
比如下面的代碼:
@Test public void test0(){ float a = 1; float b = 0.9f; System.out.println(a - b); }
結(jié)果是多少?0.1嗎?不是,執(zhí)行上面代碼執(zhí)行的結(jié)果是0.100000024。之所以產(chǎn)生這樣的結(jié)果,是因為0.1的二進(jìn)制表示是無限循環(huán)的。由于計算機的資源是有限的,所以是沒辦法用二進(jìn)制精確的表示 0.1,只能用「近似值」來表示,就是在有限的精度情況下,最大化接近 0.1 的二進(jìn)制數(shù),于是就會造成精度缺失的情況。
關(guān)于上述的現(xiàn)象大家都知道,不再詳細(xì)展開。同時,還會得出結(jié)論在科學(xué)計數(shù)法時可考慮使用浮點類型,但如果是涉及到金額計算要使用BigDecimal來計算。
那么,BigDecimal就一定能避免上述的浮點問題嗎?來看下面的示例:
@Test public void test1(){ BigDecimal a = new BigDecimal(0.01); BigDecimal b = BigDecimal.valueOf(0.01); System.out.println("a = " + a); System.out.println("b = " + b); }
上述單元測試中的代碼,a和b結(jié)果分別是什么?
a = 0.01000000000000000020816681711721685132943093776702880859375
b = 0.01
上面的實例說明,即便是使用BigDecimal,結(jié)果依舊會出現(xiàn)精度問題。這就涉及到創(chuàng)建BigDecimal對象時,如果有初始值,是采用new BigDecimal的形式,還是通過BigDecimal#valueOf方法了。
之所以會出現(xiàn)上述現(xiàn)象,是因為new BigDecimal時,傳入的0.1已經(jīng)是浮點類型了,鑒于上面說的這個值只是近似值,在使用new BigDecimal時就把這個近似值完整的保留下來了。
而BigDecimal#valueOf則不同,它的源碼實現(xiàn)如下:
public static BigDecimal valueOf(double val) { // Reminder: a zero double returns '0.0', so we cannot fastpath // to use the constant ZERO. This might be important enough to // justify a factory approach, a cache, or a few private // constants, later. return new BigDecimal(Double.toString(val)); }
在valueOf內(nèi)部,使用Double#toString方法,將浮點類型的值轉(zhuǎn)換成了字符串,因此就不存在精度丟失問題了。
此時就得出一個基本的結(jié)論:第一,在使用BigDecimal構(gòu)造函數(shù)時,盡量傳遞字符串而非浮點類型;第二,如果無法滿足第一條,則可采用BigDecimal#valueOf方法來構(gòu)造初始化值。
這里延伸一下,BigDecimal常見的構(gòu)造方法有如下幾種:
BigDecimal(int) 創(chuàng)建一個具有參數(shù)所指定整數(shù)值的對象。
BigDecimal(double) 創(chuàng)建一個具有參數(shù)所指定雙精度值的對象。
BigDecimal(long) 創(chuàng)建一個具有參數(shù)所指定長整數(shù)值的對象。
BigDecimal(String) 創(chuàng)建一個具有參數(shù)所指定以字符串表示的數(shù)值的對象。
其中涉及到參數(shù)類型為double的構(gòu)造方法,會出現(xiàn)上述的問題,使用時需特別留意。
第二:浮點精度的坑
如果比較兩個BigDecimal的值是否相等,你會如何比較?使用equals方法還是compareTo方法呢?
先來看一個示例:
@Test public void test2(){ BigDecimal a = new BigDecimal("0.01"); BigDecimal b = new BigDecimal("0.010"); System.out.println(a.equals(b)); System.out.println(a.compareTo(b)); }
乍一看感覺可能相等,但實際上它們的本質(zhì)并不相同。
equals方法是基于BigDecimal實現(xiàn)的equals方法來進(jìn)行比較的,直觀印象就是比較兩個對象是否相同,那么代碼是如何實現(xiàn)的呢?
@Override 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()); }
仔細(xì)閱讀代碼可以看出,equals方法不僅比較了值是否相等,還比較了精度是否相同。上述示例中,由于兩者的精度不同,所以equals方法的結(jié)果當(dāng)然是false了。而compareTo方法實現(xiàn)了Comparable接口,真正比較的是值的大小,返回的值為-1(小于),0(等于),1(大于)。
基本結(jié)論:通常情況,如果比較兩個BigDecimal值的大小,采用其實現(xiàn)的compareTo方法;如果嚴(yán)格限制精度的比較,那么則可考慮使用equals方法。
另外,這種場景在比較0值的時候比較常見,比如比較BigDecimal("0")、BigDecimal("0.0")、BigDecimal("0.00"),此時一定要使用compareTo方法進(jìn)行比較。
第三:設(shè)置精度的坑
在項目中看到好多同學(xué)通過BigDecimal進(jìn)行計算時不設(shè)置計算結(jié)果的精度和舍入模式,真是著急人,雖然大多數(shù)情況下不會出現(xiàn)什么問題。但下面的場景就不一定了:
@Test public void test3(){ BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("3.0"); a.divide(b); }
執(zhí)行上述代碼的結(jié)果是什么?ArithmeticException異常!
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. at java.math.BigDecimal.divide(BigDecimal.java:1690) ...
這個異常的發(fā)生在官方文檔中也有說明:
If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.
總結(jié)一下就是,如果在除法(divide)運算過程中,如果商是一個無限小數(shù)(0.333…),而操作的結(jié)果預(yù)期是一個精確的數(shù)字,那么將會拋出ArithmeticException
異常。
此時,只需在使用divide方法時指定結(jié)果的精度即可:
@Test public void test3(){ BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("3.0"); BigDecimal c = a.divide(b, 2,RoundingMode.HALF_UP); System.out.println(c); }
執(zhí)行上述代碼,輸入結(jié)果為0.33。
基本結(jié)論:在使用BigDecimal進(jìn)行(所有)運算時,一定要明確指定精度和舍入模式。
拓展一下,舍入模式定義在RoundingMode枚舉類中,共有8種:
- RoundingMode.UP:舍入遠(yuǎn)離零的舍入模式。在丟棄非零部分之前始終增加數(shù)字(始終對非零舍棄部分前面的數(shù)字加1)。注意,此舍入模式始終不會減少計算值的大小。
- RoundingMode.DOWN:接近零的舍入模式。在丟棄某部分之前始終不增加數(shù)字(從不對舍棄部分前面的數(shù)字加1,即截短)。注意,此舍入模式始終不會增加計算值的大小。
- RoundingMode.CEILING:接近正無窮大的舍入模式。如果 BigDecimal 為正,則舍入行為與 ROUNDUP 相同;如果為負(fù),則舍入行為與 ROUNDDOWN 相同。注意,此舍入模式始終不會減少計算值。
- RoundingMode.FLOOR:接近負(fù)無窮大的舍入模式。如果 BigDecimal 為正,則舍入行為與 ROUNDDOWN 相同;如果為負(fù),則舍入行為與 ROUNDUP 相同。注意,此舍入模式始終不會增加計算值。
- RoundingMode.HALF_UP:向“最接近的”數(shù)字舍入,如果與兩個相鄰數(shù)字的距離相等,則為向上舍入的舍入模式。如果舍棄部分 >= 0.5,則舍入行為與 ROUND_UP 相同;否則舍入行為與 ROUND_DOWN 相同。注意,這是我們在小學(xué)時學(xué)過的舍入模式(四舍五入)。
- RoundingMode.HALF_DOWN:向“最接近的”數(shù)字舍入,如果與兩個相鄰數(shù)字的距離相等,則為上舍入的舍入模式。如果舍棄部分 > 0.5,則舍入行為與 ROUND_UP 相同;否則舍入行為與 ROUND_DOWN 相同(五舍六入)。
- RoundingMode.HALF_EVEN:向“最接近的”數(shù)字舍入,如果與兩個相鄰數(shù)字的距離相等,則向相鄰的偶數(shù)舍入。如果舍棄部分左邊的數(shù)字為奇數(shù),則舍入行為與 ROUNDHALFUP 相同;如果為偶數(shù),則舍入行為與 ROUNDHALF_DOWN 相同。注意,在重復(fù)進(jìn)行一系列計算時,此舍入模式可以將累加錯誤減到最小。此舍入模式也稱為“銀行家舍入法”,主要在美國使用。四舍六入,五分兩種情況。如果前一位為奇數(shù),則入位,否則舍去。以下例子為保留小數(shù)點1位,那么這種舍入方式下的結(jié)果。1.15 ==> 1.2 ,1.25 ==> 1.2
- RoundingMode.UNNECESSARY:斷言請求的操作具有精確的結(jié)果,因此不需要舍入。如果對獲得精確結(jié)果的操作指定此舍入模式,則拋出ArithmeticException。
通常我們使用的四舍五入即RoundingMode.HALF_UP。
第四:三種字符串輸出的坑
當(dāng)使用BigDecimal之后,需要轉(zhuǎn)換成String類型,你是如何操作的?直接toString?
先來看看下面的代碼:
@Test public void test4(){ BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902); System.out.println(a.toString()); }
執(zhí)行的結(jié)果是上述對應(yīng)的值嗎?并不是:
3.563453525545672E+16
也就是說,本來想打印字符串的,結(jié)果打印出來的是科學(xué)計數(shù)法的值。
這里我們需要了解BigDecimal轉(zhuǎn)換字符串的三個方法
- toPlainString():不使用任何科學(xué)計數(shù)法;
- toString():在必要的時候使用科學(xué)計數(shù)法;
- toEngineeringString() :在必要的時候使用工程計數(shù)法。類似于科學(xué)計數(shù)法,只不過指數(shù)的冪都是3的倍數(shù),這樣方便工程上的應(yīng)用,因為在很多單位轉(zhuǎn)換的時候都是10^3;
三種方法展示結(jié)果示例如下:
基本結(jié)論:根據(jù)數(shù)據(jù)結(jié)果展示格式不同,采用不同的字符串輸出方法,通常使用比較多的方法為toPlainString() 。
另外,NumberFormat類的format()方法可以使用BigDecimal對象作為其參數(shù),可以利用BigDecimal對超出16位有效數(shù)字的貨幣值,百分值,以及一般數(shù)值進(jìn)行格式化控制。
使用示例如下:
NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立貨幣格式化引用 NumberFormat percent = NumberFormat.getPercentInstance(); //建立百分比格式化引用 percent.setMaximumFractionDigits(3); //百分比小數(shù)點最多3位 BigDecimal loanAmount = new BigDecimal("15000.48"); //金額 BigDecimal interestRate = new BigDecimal("0.008"); //利率 BigDecimal interest = loanAmount.multiply(interestRate); //相乘 System.out.println("金額:\t" + currency.format(loanAmount)); System.out.println("利率:\t" + percent.format(interestRate)); System.out.println("利息:\t" + currency.format(interest));
輸出結(jié)果如下:
金額: ¥15,000.48
利率: 0.8%
利息: ¥120.00
小結(jié)
本篇文章介紹了BigDecimal使用中場景的坑,以及基于這些坑我們得出的“最佳實踐”。雖然某些場景下推薦使用BigDecimal,它能夠達(dá)到更好的精度,但性能相較于double和float,還是有一定的損失的,特別在處理龐大,復(fù)雜的運算時尤為明顯。故一般精度的計算沒必要使用BigDecimal。而必須使用時,一定要規(guī)避上述的坑,更多關(guān)于java開發(fā)BigDecimal避坑的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java開發(fā)之普通web項目轉(zhuǎn)為Maven項目的方法
這篇文章主要給大家介紹了關(guān)于Java開發(fā)之普通web項目轉(zhuǎn)為Maven項目的相關(guān)資料,文中通過圖文將轉(zhuǎn)換的方法步驟介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-12-12Spring Boot整合EasyExcel(完整版包含上傳解析excel和下載模板)
這篇文章主要介紹了Spring Boot整合EasyExcel(完整版包含上傳解析excel和下載模板),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12spring cloud 使用Hystrix 實現(xiàn)斷路器進(jìn)行服務(wù)容錯保護(hù)的方法
本篇文章主要介紹了spring cloud 使用Hystrix 實現(xiàn)斷路器進(jìn)行服務(wù)容錯保護(hù)的方法,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05springboot如何實現(xiàn)導(dǎo)入其他配置類
這篇文章主要介紹了springboot如何實現(xiàn)導(dǎo)入其他配置類問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-11-11