java BigDecimal精度丟失及常見問分析
概述
作為JAVA程序員,應(yīng)該或多或少跟BigDecimal打過交道。JAVA在java.math包中提供的API類BigDecimal,用來對超過16位有效位的數(shù)進(jìn)行精確的運(yùn)算。
精度丟失
先從1個(gè)問題說起,看如下代碼
System.out.println(0.1 + 0.2);
最后打印出的結(jié)果是0.30000000000000004
,而不是預(yù)期的0.3。
有經(jīng)驗(yàn)的開發(fā)同學(xué)應(yīng)該一下子看出來這就是因?yàn)閐ouble丟失精度導(dǎo)致。更深層次的原因,是因?yàn)槲覀兊挠?jì)算機(jī)底層是二進(jìn)制的,只有0和1,對于整數(shù)來說,從低到高的每1位代表了1、2、4、8、16...這樣的2的正次數(shù)冪,只要位數(shù)足夠,每個(gè)整數(shù)都可以分解成這樣的2的正次數(shù)冪組合,例如7D=111B
,13D=1101B
。但是到了小數(shù)這里,就會(huì)發(fā)現(xiàn)2的負(fù)次數(shù)冪值是0.5、0.25、0.125、0.0625這樣的值,但是并不是每個(gè)小數(shù)都可以分解成這樣的2的負(fù)次數(shù)冪組合,例如你無法精確湊出0.1。所以,double的0.1其實(shí)并不是精確的0.1,只是通過幾個(gè)2的負(fù)次數(shù)冪值湊的近似的0.1,所以會(huì)出現(xiàn)前面0.1 + 0.2 = 0.30000000000000004
這樣的結(jié)果。
適用場景
雙精度浮點(diǎn)型變量double可以處理16位有效數(shù),但是某些場景下,即使已經(jīng)做到了16位有效位的數(shù)還是不夠,比如涉及金額計(jì)算,差一點(diǎn)就會(huì)導(dǎo)致賬目不平。
常用方法
加減乘除
既然BigDecimal主要用于數(shù)值計(jì)算,那么最基礎(chǔ)的方法就是加減乘除。BigDecimal沒有對應(yīng)的數(shù)值類的基本數(shù)據(jù)類型,所以不能直接使用+
、-
、*
、/
這樣的符號來進(jìn)行計(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
方法都是有返回值的,返回值是一個(gè)新的BigDecimal對象,原來的BigDecimal值并沒有變。
設(shè)置精度和舍入策略
可以通過setScale方法來設(shè)置精度和舍入策略。
public BigDecimal setScale(int newScale, RoundingMode roundingMode)
第1個(gè)參數(shù)newScale代表精度,即小數(shù)點(diǎn)后位數(shù);第2個(gè)參數(shù)roundingMode代表舍入策略,RoundingMode是一個(gè)枚舉,用來替代原來在BigDecimal定義的常量,原來在BigDecimal定義的常量已經(jīng)標(biāo)記為Deprecated
。在RoundingMode類中也通過1個(gè)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"); }; }
我們逐一看一下每個(gè)值的含義
- UP
直接進(jìn)位,例如下面代碼結(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
就是我們正常理解的四舍五入,實(shí)際上應(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是進(jìn)位處理,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);
實(shí)際輸出的結(jié)果是0.1000000000000000055511151231257827021181583404541015625
。其實(shí)這跟我們開篇引出的精度丟失是同一個(gè)問題,這里構(gòu)造方法中的參數(shù)0.1是double類型,本身無法精確表示0.1,雖然BigDecimal并不會(huì)導(dǎo)致精度丟失,但是在更加上游的源頭,double類型的0.1已經(jīng)丟失了精度,這里用一個(gè)已經(jīng)丟失精度的0.1來創(chuàng)建不會(huì)丟失精度的BigDecimal,精度還是會(huì)丟失。類似于使用2K的清晰度重新錄制了一遍原始只有360P的視頻,清晰度也不會(huì)優(yōu)于原始的360P。
所以,我們應(yīng)該盡量避免使用double來創(chuàng)建BigDecimal,確實(shí)源頭是double的,我們可以使用valueOf方法,這個(gè)方法會(huì)先調(diào)用Double.toString(val)
來轉(zhuǎn)成String,這樣就不會(huì)產(chǎn)生精度丟失,下面的代碼結(jié)果就是0.1
BigDecimal a = BigDecimal.valueOf(0.1); System.out.println(a);
順便說一下,BigDecimal還內(nèi)置了ZERO
、ONE
、TEN
這樣的常量可以直接使用。
toString
這個(gè)問題比較隱蔽,在數(shù)據(jù)比較小的時(shí)候不會(huì)遇到,但是看如下代碼
BigDecimal a = BigDecimal.valueOf(987654321987654321.123456789123456789); System.out.println(a);
最后實(shí)際輸出的結(jié)果是9.8765432198765427E+17
。原因是System.out.println會(huì)自動(dòng)調(diào)用BigDecimal的toString
方法,而這個(gè)方法會(huì)在必要時(shí)使用科學(xué)計(jì)數(shù)法,如果不想使用科學(xué)計(jì)數(shù)法,可以使用BigDecimal的toPlainString
方法。另外提一下,BigDecimal還提供了一個(gè)toEngineeringString
方法,這個(gè)方法也會(huì)使用科學(xué)技術(shù)法,不一樣的是,這里面的10都是3、6、9這樣的冪,對應(yīng)我們在查看大數(shù)的時(shí)候,很多都是每3位會(huì)增加1個(gè)逗號。
comparTo 和 equals
這個(gè)問題出現(xiàn)的不多,有經(jīng)驗(yàn)的開發(fā)同學(xué)在比較數(shù)值的時(shí)候,會(huì)自然而然使用comparTo方法。這里說一下BigDecimal的equals方法除了比較數(shù)值之外,還會(huì)比較scale精度,不同精度不會(huì)equles。
例如下面代碼分別會(huì)返回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));
不能除盡時(shí)ArithmeticException異常
上面提到的加減乘除的4個(gè)方法中,除法會(huì)比較特殊,因?yàn)榭赡艹霈F(xiàn)除不盡的情況,這時(shí)如果沒有設(shè)置精度,就會(huì)拋出ArithmeticException,因?yàn)檫@個(gè)是否能除盡是跟具體數(shù)值相關(guān)的,這會(huì)導(dǎo)致偶現(xiàn)的bug,更加難以排查。
例如下面代碼就會(huì)拋出ArithmeticException異常
BigDecimal a = new BigDecimal(1); BigDecimal b = new BigDecimal(3); System.out.println(a.divide(b));
應(yīng)對的方法是,在除法運(yùn)算時(shí),注意設(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àn)榫葋G失而不滿足的某些特殊業(yè)務(wù)場景,例如會(huì)計(jì)金額計(jì)算。在可以忍受略微不精確的場景還是使用內(nèi)部提供的add
、subtract
、multiply
、divide
方法來進(jìn)行基礎(chǔ)的加減乘除運(yùn)算,運(yùn)算后會(huì)返回新的對象,原始的對象并不會(huì)改變。在使用BigDecimal的過程中,要注意創(chuàng)建對象、toString、比較數(shù)值、不能除盡時(shí)需要設(shè)置精度等問題。
以上就是java BigDecimal精度丟失及常見問分析的詳細(xì)內(nèi)容,更多關(guān)于java BigDecimal精度丟失的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Spring cloud oauth2如何搭建認(rèn)證資源中心
這篇文章主要介紹了Spring cloud oauth2如何搭建認(rèn)證資源中心,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11Java實(shí)現(xiàn)深度優(yōu)先搜索(DFS)和廣度優(yōu)先搜索(BFS)算法
深度優(yōu)先搜索(DFS)和廣度優(yōu)先搜索(BFS)是兩種基本的圖搜索算法,可用于圖的遍歷、路徑搜索等問題。DFS采用棧結(jié)構(gòu)實(shí)現(xiàn),從起點(diǎn)開始往深處遍歷,直到找到目標(biāo)節(jié)點(diǎn)或遍歷完整個(gè)圖;BFS采用隊(duì)列結(jié)構(gòu)實(shí)現(xiàn),從起點(diǎn)開始往廣處遍歷,直到找到目標(biāo)節(jié)點(diǎn)或遍歷完整個(gè)圖2023-04-04springboot+dubbo啟動(dòng)項(xiàng)目時(shí)報(bào)錯(cuò) zookeeper not connect
這篇文章主要介紹了springboot+dubbo項(xiàng)目啟動(dòng)項(xiàng)目時(shí)報(bào)錯(cuò) zookeeper not connected的問題,本文給大家定位問題及解決方案,結(jié)合實(shí)例代碼給大家講解的非常詳細(xì),需要的朋友可以參考下2023-06-06用java的spring實(shí)現(xiàn)一個(gè)簡單的IOC容器示例代碼
本篇文章主要介紹了用java實(shí)現(xiàn)一個(gè)簡單的IOC容器示例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-03-03Java關(guān)于遠(yuǎn)程調(diào)試程序教程(以Eclipse為例)
這篇文章主要介紹了Java關(guān)于遠(yuǎn)程調(diào)試程序教程(以Eclipse為例),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-06-06詳解Spring Security的formLogin登錄認(rèn)證模式
對于一個(gè)完整的應(yīng)用系統(tǒng),與登錄驗(yàn)證相關(guān)的頁面都是高度定制化的,非常美觀而且提供多種登錄方式。這就需要Spring Security支持我們自己定制登錄頁面,也就是本文給大家介紹的formLogin模式登錄認(rèn)證模式,感興趣的朋友跟隨小編一起看看吧2019-11-11