java BigDecimal精度丟失及常見問分析
概述
作為JAVA程序員,應(yīng)該或多或少跟BigDecimal打過交道。JAVA在java.math包中提供的API類BigDecimal,用來對超過16位有效位的數(shù)進行精確的運算。
精度丟失
先從1個問題說起,看如下代碼
System.out.println(0.1 + 0.2);
最后打印出的結(jié)果是0.30000000000000004,而不是預(yù)期的0.3。
有經(jīng)驗的開發(fā)同學(xué)應(yīng)該一下子看出來這就是因為double丟失精度導(dǎo)致。更深層次的原因,是因為我們的計算機底層是二進制的,只有0和1,對于整數(shù)來說,從低到高的每1位代表了1、2、4、8、16...這樣的2的正次數(shù)冪,只要位數(shù)足夠,每個整數(shù)都可以分解成這樣的2的正次數(shù)冪組合,例如7D=111B,13D=1101B。但是到了小數(shù)這里,就會發(fā)現(xiàn)2的負(fù)次數(shù)冪值是0.5、0.25、0.125、0.0625這樣的值,但是并不是每個小數(shù)都可以分解成這樣的2的負(fù)次數(shù)冪組合,例如你無法精確湊出0.1。所以,double的0.1其實并不是精確的0.1,只是通過幾個2的負(fù)次數(shù)冪值湊的近似的0.1,所以會出現(xiàn)前面0.1 + 0.2 = 0.30000000000000004這樣的結(jié)果。
適用場景
雙精度浮點型變量double可以處理16位有效數(shù),但是某些場景下,即使已經(jīng)做到了16位有效位的數(shù)還是不夠,比如涉及金額計算,差一點就會導(dǎo)致賬目不平。
常用方法
加減乘除
既然BigDecimal主要用于數(shù)值計算,那么最基礎(chǔ)的方法就是加減乘除。BigDecimal沒有對應(yīng)的數(shù)值類的基本數(shù)據(jù)類型,所以不能直接使用+、-、*、/這樣的符號來進行計算,而要使用BigDecimal內(nèi)部的方法。
public BigDecimal add(BigDecimal augend) public BigDecimal subtract(BigDecimal subtrahend) public BigDecimal multiply(BigDecimal multiplicand) public BigDecimal divide(BigDecimal divisor)
需要注意的是,BigDecimal是不可變的,所以,add、subtract、multiply、divide方法都是有返回值的,返回值是一個新的BigDecimal對象,原來的BigDecimal值并沒有變。
設(shè)置精度和舍入策略
可以通過setScale方法來設(shè)置精度和舍入策略。
public BigDecimal setScale(int newScale, RoundingMode roundingMode)
第1個參數(shù)newScale代表精度,即小數(shù)點后位數(shù);第2個參數(shù)roundingMode代表舍入策略,RoundingMode是一個枚舉,用來替代原來在BigDecimal定義的常量,原來在BigDecimal定義的常量已經(jīng)標(biāo)記為Deprecated。在RoundingMode類中也通過1個valueOf方法來給出映射關(guān)系
/**
* Returns the {@code RoundingMode} object corresponding to a
* legacy integer rounding mode constant in {@link BigDecimal}.
*
* @param rm legacy integer rounding mode to convert
* @return {@code RoundingMode} corresponding to the given integer.
* @throws IllegalArgumentException integer is out of range
*/
public static RoundingMode valueOf(int rm) {
return switch (rm) {
case BigDecimal.ROUND_UP -> UP;
case BigDecimal.ROUND_DOWN -> DOWN;
case BigDecimal.ROUND_CEILING -> CEILING;
case BigDecimal.ROUND_FLOOR -> FLOOR;
case BigDecimal.ROUND_HALF_UP -> HALF_UP;
case BigDecimal.ROUND_HALF_DOWN -> HALF_DOWN;
case BigDecimal.ROUND_HALF_EVEN -> HALF_EVEN;
case BigDecimal.ROUND_UNNECESSARY -> UNNECESSARY;
default -> throw new IllegalArgumentException("argument out of range");
};
}
我們逐一看一下每個值的含義
- UP
直接進位,例如下面代碼結(jié)果是3.15
BigDecimal pi = BigDecimal.valueOf(3.141); System.out.println(pi.setScale(2, RoundingMode.UP));
- DOWN
直接舍去,例如下面代碼結(jié)果是3.1415
BigDecimal pi = BigDecimal.valueOf(3.14159); System.out.println(pi.setScale(4, RoundingMode.DOWN));
- CEILING
如果是正數(shù),相當(dāng)于UP;如果是負(fù)數(shù),相當(dāng)于DOWN。 - FLOOR
如果是正數(shù),相當(dāng)于DOWN;如果是負(fù)數(shù),相當(dāng)于UP。 - HALF_UP
就是我們正常理解的四舍五入,實際上應(yīng)該也是最常用的。 下面的代碼結(jié)果是3.14
BigDecimal pi = BigDecimal.valueOf(3.14159); System.out.println(pi.setScale(2, RoundingMode.HALF_UP));
下面的代碼結(jié)果是3.142
BigDecimal pi = BigDecimal.valueOf(3.14159); System.out.println(pi.setScale(3, RoundingMode.HALF_UP));
- HALF_DOWN
與四舍五入類似,這種是五舍六入。我們對于HALF_UP和HALF_DOWN可以理解成對于5的處理不同,UP遇到5是進位處理,DOWN遇到5是舍去處理, - HALF_EVEN
如果舍棄部分左邊的數(shù)字為偶數(shù),相當(dāng)于HALF_DOWN;如果舍棄部分左邊的數(shù)字為奇數(shù),相當(dāng)于HALF_UP - UNNECESSARY
非必要舍入。如果除去小數(shù)的后導(dǎo)0后,位數(shù)小于等于scale,那么就是去除scale位數(shù)后面的后導(dǎo)0;位數(shù)大于scale,拋出ArithmeticException。
下面代碼結(jié)果是3.14
BigDecimal pi = BigDecimal.valueOf(3.1400); System.out.println(pi.setScale(2, RoundingMode.UNNECESSARY));
下面代碼拋出ArithmeticException
BigDecimal pi = BigDecimal.valueOf(3.1400); System.out.println(pi.setScale(1, RoundingMode.UNNECESSARY));
常見問題
創(chuàng)建BigDecimal對象
先看下面代碼
BigDecimal a = new BigDecimal(0.1); System.out.println(a);
實際輸出的結(jié)果是0.1000000000000000055511151231257827021181583404541015625。其實這跟我們開篇引出的精度丟失是同一個問題,這里構(gòu)造方法中的參數(shù)0.1是double類型,本身無法精確表示0.1,雖然BigDecimal并不會導(dǎo)致精度丟失,但是在更加上游的源頭,double類型的0.1已經(jīng)丟失了精度,這里用一個已經(jīng)丟失精度的0.1來創(chuàng)建不會丟失精度的BigDecimal,精度還是會丟失。類似于使用2K的清晰度重新錄制了一遍原始只有360P的視頻,清晰度也不會優(yōu)于原始的360P。
所以,我們應(yīng)該盡量避免使用double來創(chuàng)建BigDecimal,確實源頭是double的,我們可以使用valueOf方法,這個方法會先調(diào)用Double.toString(val)來轉(zhuǎn)成String,這樣就不會產(chǎn)生精度丟失,下面的代碼結(jié)果就是0.1
BigDecimal a = BigDecimal.valueOf(0.1); System.out.println(a);
順便說一下,BigDecimal還內(nèi)置了ZERO、ONE、TEN這樣的常量可以直接使用。
toString
這個問題比較隱蔽,在數(shù)據(jù)比較小的時候不會遇到,但是看如下代碼
BigDecimal a = BigDecimal.valueOf(987654321987654321.123456789123456789); System.out.println(a);
最后實際輸出的結(jié)果是9.8765432198765427E+17。原因是System.out.println會自動調(diào)用BigDecimal的toString方法,而這個方法會在必要時使用科學(xué)計數(shù)法,如果不想使用科學(xué)計數(shù)法,可以使用BigDecimal的toPlainString方法。另外提一下,BigDecimal還提供了一個toEngineeringString方法,這個方法也會使用科學(xué)技術(shù)法,不一樣的是,這里面的10都是3、6、9這樣的冪,對應(yīng)我們在查看大數(shù)的時候,很多都是每3位會增加1個逗號。
comparTo 和 equals
這個問題出現(xiàn)的不多,有經(jīng)驗的開發(fā)同學(xué)在比較數(shù)值的時候,會自然而然使用comparTo方法。這里說一下BigDecimal的equals方法除了比較數(shù)值之外,還會比較scale精度,不同精度不會equles。
例如下面代碼分別會返回0和false
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.10");
System.out.println(a.compareTo(b));
System.out.println(a.equals(b));
不能除盡時ArithmeticException異常
上面提到的加減乘除的4個方法中,除法會比較特殊,因為可能出現(xiàn)除不盡的情況,這時如果沒有設(shè)置精度,就會拋出ArithmeticException,因為這個是否能除盡是跟具體數(shù)值相關(guān)的,這會導(dǎo)致偶現(xiàn)的bug,更加難以排查。
例如下面代碼就會拋出ArithmeticException異常
BigDecimal a = new BigDecimal(1); BigDecimal b = new BigDecimal(3); System.out.println(a.divide(b));
應(yīng)對的方法是,在除法運算時,注意設(shè)置結(jié)果的精度和舍入模式,下面的代碼就能正常輸出結(jié)果0.33
BigDecimal a = new BigDecimal(1); BigDecimal b = new BigDecimal(3); System.out.println(a.divide(b, 2, RoundingMode.HALF_UP));
總結(jié)
BigDecimal主要用于double因為精度丟失而不滿足的某些特殊業(yè)務(wù)場景,例如會計金額計算。在可以忍受略微不精確的場景還是使用內(nèi)部提供的add、subtract、multiply、divide方法來進行基礎(chǔ)的加減乘除運算,運算后會返回新的對象,原始的對象并不會改變。在使用BigDecimal的過程中,要注意創(chuàng)建對象、toString、比較數(shù)值、不能除盡時需要設(shè)置精度等問題。
以上就是java BigDecimal精度丟失及常見問分析的詳細(xì)內(nèi)容,更多關(guān)于java BigDecimal精度丟失的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java模擬http的Get/Post請求,并設(shè)置ip與port代理的方法
下面小編就為大家?guī)硪黄猨ava模擬http的Get/Post請求,并設(shè)置ip與port代理的方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-02-02
淺談關(guān)于Java正則和轉(zhuǎn)義中\(zhòng)\和\\\\的理解
這篇文章主要介紹了淺談關(guān)于Java正則和轉(zhuǎn)義中\(zhòng)\和\\\\的理解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
詳解AngularJs與SpringMVC簡單結(jié)合使用
本篇文章主要介紹了AngularJs與SpringMVC簡單結(jié)合使用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-06-06
Java線程之鎖對象Lock-同步問題更完美的處理方式代碼實例
這篇文章主要介紹了Java線程之鎖對象Lock-同步問題更完美的處理方式代碼實例,還是挺不錯的,這里分享給大家,需要的朋友可以參考。2017-11-11
Java隨手筆記8之包、環(huán)境變量和訪問控制及maven profile實現(xiàn)多環(huán)境打包
這篇文章主要介紹了Java隨手筆記8之包、環(huán)境變量和訪問控制及maven profile實現(xiàn)多環(huán)境打包的相關(guān)資料,需要的朋友可以參考下2015-11-11

