1. 概述
BigDecimal是Java在java.math包中提供的線程安全的API類。BigDecimal是Java中用于表示任意精度數(shù)字的類,它可以表示無限長度的小數(shù),BigDecimal 通常支持任意位數(shù)的小數(shù)部分,用來對超過16位有效位的數(shù)進(jìn)行精確的運算。雙精度浮點型變量double可以處理16位有效數(shù),但在實際應(yīng)用中,可能需要對更大或者更小的數(shù)進(jìn)行運算和處理。一般情況下,對于那些不需要準(zhǔn)確計算精度的數(shù)字,我們可以直接使用Float和Double處理,但是Double.valueOf(String) 和Float.valueOf(String)會丟失精度。所以開發(fā)中,如果我們需要精確計算的結(jié)果,則必須使用BigDecimal類來操作。
? BigDecimal所創(chuàng)建的是對象,故我們不能使用傳統(tǒng)的+、-、*、/等算術(shù)運算符直接對其對象進(jìn)行數(shù)學(xué)運算,而必須調(diào)用其相對應(yīng)的方法。方法中的參數(shù)也必須是BigDecimal的對象。構(gòu)造器是類的特殊方法,專門用來創(chuàng)建對象,特別是帶有參數(shù)的對象。
2. 常用構(gòu)造方法

3. BigDecimal常用方法
注意:BigDecimal進(jìn)行運算時必須要保證對象本身不能是null,否則就會拋空指針異常。

代碼示例:
import java.math.BigDecimal; public class Test { public static void main(String[] args){ BigDecimal b1 = new BigDecimal("1"); BigDecimal b2 = new BigDecimal("2"); BigDecimal b3 = new BigDecimal("4"); System.out.println("相加:"+b1.add(b2)); System.out.println("相減:"+b1.subtract(b2)); System.out.println("相乘:"+b2.multiply(b3)); System.out.println("相除:"+b2.divide(b3)); } }
4. 進(jìn)階
4.1、BigDecimal的八種舍入模式
BigDecimal.setScale()方法用于格式化小數(shù)點
setScale(1)表示保留一位小數(shù),默認(rèn)用四舍五入方式
setScale(1,BigDecimal.ROUND_DOWN)直接刪除多余的小數(shù)位,如2.35會變成2.3
setScale(1,BigDecimal.ROUND_UP)進(jìn)位處理,2.35變成2.4
setScale(1,BigDecimal.ROUND_HALF_UP)四舍五入,2.35變成2.4
setScaler(1,BigDecimal.ROUND_HALF_DOWN)四舍五入,2.35變成2.3,如果是5則向下舍
setScaler(1,BigDecimal.ROUND_CEILING)接近正無窮大的舍入
setScaler(1,BigDecimal.ROUND_FLOOR)接近負(fù)無窮大的舍入,數(shù)字>0和ROUND_UP作用一樣,數(shù)字<0和ROUND_DOWN作用一樣
setScaler(1,BigDecimal.ROUND_HALF_EVEN)向最接近的數(shù)字舍入,如果與兩個相鄰數(shù)字的距離相等,則向相鄰的偶數(shù)舍入。
1、ROUND_UP,向遠(yuǎn)離0的方向舍入,在丟棄非零部分之前始終增加數(shù)字(始終對非零舍棄部分前面的數(shù)字加1)。
注意,此舍入模式始終不會減少計算值的大小。
eg: 保留1位小數(shù) 1.60->1.6 1.61->1.7 1.66->1.7 , -1.62->-1.7
2、ROUND_DOWN,向接近0的方向舍入,在丟棄某部分之前,始終不增加數(shù)據(jù)(即,截斷),該方式是只減不加。
eg: 保留1位小數(shù) 1.60->1.6 1.61->1.6 1.66->1.6 , -1.62->-1.6
3、ROUND_CEILING,向正無窮方向舍入,如果數(shù)值為正,舍入方式與ROUND_UP一致,如果為負(fù),舍入方式與ROUND_DOWN一致,該模式始終不會減少計算數(shù)值。
eg: 保留1位小數(shù) 1.60->1.6 1.61->1.7 1.66->1.7 , -1.62->-1.6
4、ROUND_FLOOR,向負(fù)無窮方向舍入,如果數(shù)值為正,舍入行為與 ROUND_DOWN 相同;如果為負(fù),則舍入行為與 ROUND_UP 相同。該模式始終不會增加計算數(shù)值。
eg: 保留1位小數(shù) 1.60->1.6 1.61->1.6 1.66->1.6 , -1.62->-1.7
5、ROUND_HALF_UP,向“最接近的”數(shù)字舍入,也就是四舍五入。
eg: 保留1位小數(shù) 1.61->1.6 1.65->1.7 1.66->1.7 , -1.62->-1.6
6、ROUND_HALF_DOWN,向“最接近的”數(shù)字舍入,如果與兩個相鄰數(shù)字的距離相等,則為上舍入的舍入模式,也就是五舍六入。
eg: 保留1位小數(shù) 1.61->1.6 1.65->1.6 1.66->1.7 , -1.62->-1.6
7、ROUND_HALF_EVEN,向“最接近的”數(shù)字舍入,如果與兩個相鄰數(shù)字的距離相等,則向相鄰的偶數(shù)舍入。如果舍棄部分左邊的數(shù)字為奇數(shù),則舍入行為與 ROUND_HALF_UP 相同;如果為偶數(shù),則舍入行為與 ROUND_HALF_DOWN 相同。
注意,在重復(fù)進(jìn)行一系列計算時,此舍入模式可以將累加錯誤減到最小。
此舍入模式也稱為“銀行家舍入法”,主要在美國使用。四舍六入,五分兩種情況。如果前一位為奇數(shù),則入位,否則舍去。以下例子為保留小數(shù)點1位,那么這種舍入方式下的結(jié)果。
eg. 1.15->1.2, 1.25->1.2
8、ROUND_UNNECESSARY,計算結(jié)果是精確的,不需要舍入模式。如果對獲得精確結(jié)果的操作指定此舍入模式,則拋出ArithmeticException。
BigDecimal b = new BigDecimal("1.6666"); System.out.println("result b:" + b.setScale(2, BigDecimal.ROUND_HALF_UP)); // 1.67 System.out.println("result b:" + b.setScale(2)); // 精度錯誤
result b:1.67
Exception in thread "main" java.lang.ArithmeticException: Rounding necessar
原因分析:
setScale方法默認(rèn)使用的roundingMode是ROUND_UNNECESSARY,不需要使用舍入模式,設(shè)置精度2位,但是小數(shù)點后有4位肯定會拋異常。
BigDecimal可以與DecimalFormat結(jié)合使用,從而對金額格式化,如小數(shù)點后面統(tǒng)一保留兩位,不夠兩位的補零,多余兩位的舍入。
import java.math.BigDecimal; import java.text.DecimalFormat; public class Test { public static void main(String[] s){ System.out.println(formatToNumber(new BigDecimal("12333.435"))); System.out.println(formatToNumber(new BigDecimal(0))); System.out.println(formatToNumber(new BigDecimal("0.00"))); System.out.println(formatToNumber(new BigDecimal("0.001"))); System.out.println(formatToNumber(new BigDecimal("0.006"))); System.out.println(formatToNumber(new BigDecimal("0.206"))); System.out.println(formatToNumber(new BigDecimal("1.22"))); } /** * @desc * @param obj 傳入的小數(shù) * @return */ public static String formatToNumber(BigDecimal obj) { // DecimalFormat默認(rèn)使用的是進(jìn)位方式是RoundingMode.HALF_EVEN,此舍入模式也稱為“銀行家算法”,主要在美國使用。 //銀行家算法:四舍六入五考慮,五后非零就進(jìn)一,五后為零看奇偶,五前為偶應(yīng)舍去,五前為奇要進(jìn)一 DecimalFormat df = new DecimalFormat("###,##0.00"); return df.format(obj); } }
//執(zhí)行結(jié)果
12,333.44
0.00
0.00
0.00
0.01
0.21
1.22
需注意:
DecimalFormat的默認(rèn)進(jìn)位方式不是四舍五入,所以當(dāng)小數(shù)點后面需要舍去的時候,肯能跟預(yù)想的不一樣,具體可參考《關(guān)于DecimalFormat的取舍問題,DecimalFormat四舍五入的坑》
new DecimalFormat(“###,##0.00”)小數(shù)點前面需要有個0,這樣0-1之間的數(shù)字才會正常格式化;若##0.00的小數(shù)點前面沒有0,則0-1之間的數(shù)字會被丟失掉小數(shù)點前的0,代碼如下:
import java.math.BigDecimal; import java.text.DecimalFormat; public class Test { public static void main(String[] s){ System.out.println(formatToNumber(new BigDecimal("12333.435"))); System.out.println(formatToNumber(new BigDecimal(0))); System.out.println(formatToNumber(new BigDecimal("0.00"))); System.out.println(formatToNumber(new BigDecimal("0.001"))); System.out.println(formatToNumber(new BigDecimal("0.006"))); System.out.println(formatToNumber(new BigDecimal("0.206"))); System.out.println(formatToNumber(new BigDecimal("1.22"))); } /** * @desc * @param obj 傳入的小數(shù) * @return */ public static String formatToNumber(BigDecimal obj) { // DecimalFormat默認(rèn)使用的是進(jìn)位方式是RoundingMode.HALF_EVEN,此舍入模式也稱為“銀行家算法”,主要在美國使用。 //銀行家算法:四舍六入五考慮,五后非零就進(jìn)一,五后為零看奇偶,五前為偶應(yīng)舍去,五前為奇要進(jìn)一 DecimalFormat df = new DecimalFormat("###,##.00"); return df.format(obj); } }
1,23,33.44
.00
.00
.00
.01
.21
1.22
經(jīng)常能看到金額用¥120.00表示,利率用0.8%表示,這里擴(kuò)展一下BigDecimal的貨幣格式化與百分比格式化
NumberFormat類的format()方法可以使用BigDecimal對象作為其參數(shù),可以利用BigDecimal對超出16位有效數(shù)字的貨幣值,百分值,以及一般數(shù)值進(jìn)行格式化控制。
NumberFormat對象:
getCompactNumberInstance();返回FORMAT帶有"SHORT"格式樣式的默認(rèn)語言環(huán)境 的緊湊數(shù)字格式 。
getCurrencyInstance?(Locale inLocale);返回指定語言環(huán)境的貨幣格式。若是不指定參數(shù),則以默認(rèn)語言為參數(shù)。
getInstance?(Locale inLocale);返回指定語言環(huán)境的通用數(shù)字格式。若是不指定參數(shù),則以默認(rèn)語言為參數(shù)。
getPercentInstance?(Locale inLocale);返回指定語言環(huán)境的百分比格式。若是不指定參數(shù),則以默認(rèn)語言為參數(shù)。
import java.math.BigDecimal; import java.text.NumberFormat; public class Test { public static void main(String[] args){ NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立貨幣格式化引用 NumberFormat percent = NumberFormat.getPercentInstance(); //建立百分比格式化引用 percent.setMinimumFractionDigits(2);//設(shè)置數(shù)的小數(shù)部分所允許的最小位數(shù)(如果不足后面補0) percent.setMaximumFractionDigits(3);//設(shè)置數(shù)的小數(shù)部分所允許的最大位數(shù)(如果超過會四舍五入) BigDecimal amount = new BigDecimal("250600.42"); //金額 BigDecimal interestRate = new BigDecimal("0.0004"); //利率 BigDecimal interest = amount .multiply(interestRate); //相乘 System.out.println("金額: " + currency.format(loanAmount)); System.out.println("利率: " + percent.format(interestRate)); System.out.println("利息: " + currency.format(interest)); } }
金額: ¥250,600.42
利率: 0.04%
利息: ¥100.24
NumberFormat提供了多種貨幣格式的引用,如¥(人民幣),$(美元、英元)等等
在BigDecimal 中提供了多種創(chuàng)建方式,可以通過new 直接創(chuàng)建,也可以通過 BigDecimal#valueOf 創(chuàng)建。這兩種方式使用不當(dāng),也會導(dǎo)致精度問題。如下:
public static void main(String[] args) throws Exception { BigDecimal b1= new BigDecimal(0.1); System.out.println(b1); BigDecimal b2= BigDecimal.valueOf(0.1); System.out.println(b2); BigDecimal b3= BigDecimal.valueOf(0.111111111111111111111111111234); System.out.println(b3); } 0.1000000000000000055511151231257827021181583404541015625 0.1 0.1111111111111111上面示例中兩個方法都傳入了double類型的參數(shù)0.1但是 b1 還是出現(xiàn)了精度的問題。造成這種問題的原因是 0.1 這個數(shù)字計算機是無法精確表示的,送給 BigDecimal 的時候就已經(jīng)丟精度了,而 BigDecimal#valueOf 的實現(xiàn)卻完全不同。如下源碼所示,BigDecimal#valueOf 中是把浮點數(shù)轉(zhuǎn)換成了字符串來構(gòu)造的BigDecimal,因此避免了問題。
public static BigDecimal valueOf(double val) { return new BigDecimal(Double.toString(val)); }結(jié)論:
第一,在使用BigDecimal構(gòu)造函數(shù)時,盡量傳遞字符串而非浮點類型;
第二,如果無法滿足第一條,則可采用BigDecimal#valueOf方法來構(gòu)造初始化值。但是valueOf受double類型精度影響,當(dāng)傳入?yún)?shù)小數(shù)點后的位數(shù)超過double允許的16位精度還是可能會出現(xiàn)問題的5.2、踩坑二:等值比較的坑
一般在比較兩個值是否相等時,都是用equals 方法,但是,在BigDecimal 中使用equals可能會導(dǎo)致結(jié)果錯誤,BigDecimal 中提供了 compareTo 方法,在很多時候需要使用compareTo 比較兩個值。如下所示:
public static void main(String[] args){ BigDecimal b1 = new BigDecimal("1.0"); BigDecimal b2 = new BigDecimal("1.00"); System.out.println(b1.equals(b2)); System.out.println(b1.compareTo(b2)); }false 0出現(xiàn)此種結(jié)果的原因是,equals不僅比較了值是否相等,還比較了精度是否相同。示例中,由于兩個值的精度不同,所有結(jié)果也就不相同。而 compareTo 是只比較值的大小。返回的值為-1(小于),0(等于),1(大于)。
結(jié)論
- 如果比較兩個BigDecimal值的大小,采用其實現(xiàn)的compareTo方法;
- 如果嚴(yán)格限制精度的比較,那么則可考慮使用equals方法。
5.3、踩坑三:無限精度的坑
BigDecimal 并不代表無限精度,當(dāng)在兩個數(shù)除不盡的時候,就會出現(xiàn)無限精度的坑,如下所示:
public static void main(String[] args){ BigDecimal b1 = new BigDecimal("1.0"); BigDecimal b2 = new BigDecimal("3.0"); b1.divide(b2); } Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result. at java.math.BigDecimal.divide(BigDecimal.java:1693) at com.demo.controller.Test.main(Test.java:29)大致意思就是,如果在除法(divide)運算過程中,如果商是一個無限小數(shù)(如 0.333…),而操作的結(jié)果預(yù)期是一個精確的數(shù)字,那么將會拋出ArithmeticException異常。
此種情況,只需要在使用 divide方法時指定結(jié)果的精度即可:
public static void main(String[] args){ BigDecimal b1 = new BigDecimal("1.0"); BigDecimal b2 = new BigDecimal("3.0"); System.out.println(b1.divide(b2,2, RoundingMode.HALF_UP));//0.33 }結(jié)論:
- 在使用BigDecimal進(jìn)行(所有)運算時,盡量指定精度和舍入模式。
5.4、踩坑四:BigDecimal三種字符串輸出的坑
在BigDecimal 轉(zhuǎn)換成字符串時,有可能輸出非你預(yù)期的結(jié)果。如下所示:
public static void main(String[] args){ BigDecimal bg = new BigDecimal("1E11"); System.out.println(bg.toString()); // 1E+11 System.out.println(bg.toPlainString()); // 100000000000 System.out.println(bg.toEngineeringString()); // 100E+9 }
1E+11
100000000000
100E+9
可以看到三種方式輸出的結(jié)果可能都不相同,可能這個并不是預(yù)期的結(jié)果 ,BigDecimal 有三個方法可以轉(zhuǎn)為相應(yīng)的字符串類型,切記不要用錯:
以下內(nèi)容介紹java.math.BigDecimal下的三個toString方法的區(qū)別及用法
toPlainString() : 不使用任何指數(shù)。
toString() :有必要時使用科學(xué)計數(shù)法。
toEngineeringString():有必要時使用工程計數(shù)法。 工程記數(shù)法是一種工程計算中經(jīng)常使用的記錄數(shù)字的方法,與科學(xué)技術(shù)法類似,但要求10的冪必須是3的倍數(shù)
在使用BigDecimal類型進(jìn)行計算時,進(jìn)行加、減、乘、除、比較大小時,一定要保證參與計算的兩個值不能為空,否則會拋出java.lang.NullPointerException異常。
BigDecimal b1 = new BigDecimal("1"); BigDecimal b2 = null; System.out.println("相加:"+b2.add(b1)); Exception in thread "main" java.lang.NullPointerException at com.demo.controller.Test.main(Test.java:14)
代碼示例:
BigDecimal b1 = new BigDecimal("1"); BigDecimal b2 = new BigDecimal("0"); System.out.println(b1.divide(b2)); Exception in thread "main" java.lang.ArithmeticException: Division by zero
乘法滿足交換律是一個常識,但是在計算機的世界里,會出現(xiàn)不滿足乘法交換律的情況;
代碼示例:
BigDecimal b1 = BigDecimal.valueOf(1.0); BigDecimal b2 = BigDecimal.valueOf(3.0); BigDecimal b3 = BigDecimal.valueOf(3.0); System.out.println(b1.divide(b2, 2, RoundingMode.HALF_UP).multiply(b3)); // 0.990 System.out.println(b1.multiply(b3).divide(b2, 2, RoundingMode.HALF_UP)); // 1.00
執(zhí)行順序交換后,產(chǎn)生的結(jié)果可能不同,會導(dǎo)致一定的問題,使用順序建議先乘后除。