BigDecimal精度問題
背景
案例
一次開發中碰到這樣一個問題,給定一批車總放款金額,每輛車的實際價格(整數),讓根據實際價格的比例進行計算每輛車的放款金額(整數)。
解決方案:
1.循環每輛車
2.前n-1輛車的放款金額=總放款金額*當前車的實際價格/總實際價格;
3.最後一輛車放款金額=總放款金額-(n-1)車的總放款金額;
經過幾次測試發現計算沒有問題,就發佈上線了,安全運行了一百多天,直到有一天出現了最後一輛的放款金額爲負數,一個精度問題就發生了(突然想到墨菲定律)!
通過查看日誌和數據的模擬,發現是一個四捨五入的問題,當實際價格/總實際價格的時候,如果結果是0.106經過四捨五入爲0.11,這樣前面每輛車就會多分配一些放款金額,最終導致(n-1)輛車總放款金額大於給定的總放款金額。
最終通過配置BigDecimal的RoundMode,將四捨五入改爲了捨去,這樣保證n-1輛車都不會出現多算的情況,從而解決問題。
想這種問題在實際開發中很難去發現問題,因此我們用BigDecimal一定要清楚他的API,從而避免不適當的使用。
BigDecimal的使用
Java在java.math包中提供的API類BigDecimal,
用來對超過16位有效位的數進行精確的運算。雙精度浮點型變量double可以處理16位有效數。
在實際應用中,需要對更大或者更小的數進行運算和處理。float和double只能用來做科學計算或者是工程計算,在商業計算中要用java.math.BigDecimal。BigDecimal所創建的是對象,我們不能使用傳統的+、-、*、/等算術運算符直接對其對象進行數學運算,而必須調用其相對應的方法。方法中的參數也必須是BigDecimal的對象。構造器是類的特殊方法,專門用來創建對象,特別是帶有參數的對象。
構造方法
BigDecimal一共有4個構造方法:
- BigDecimal(int) 創建一個具有參數所指定整數值的對象。
- BigDecimal(double) 創建一個具有參數所指定雙精度值的對象。(不建議採用)
- BigDecimal(long) 創建一個具有參數所指定長整數值的對象。
- BigDecimal(String) 創建一個具有參數所指定以字符串表示的數值的對象
第四個方法不建議使用是因爲double本身會有精度問題,比如:
BigDecimal a = new BigDecimal(0.1);
BigDecimal b = new BigDecimal("0.1");
BigDecimal c = BigDecimal.valueOf(0.1);
System.out.println(a);
System.out.println(b);
System.out.println(c);
System.out.println(a.equals(b));
System.out.println(b.equals(c));
輸出結果:
0.1000000000000000055511151231257827021181583404541015625
0.1
0.1
false
true
原因:JDK的描述:1、參數類型爲double的構造方法的結果有一定的不可預知性。有人可能認爲在Java中寫入newBigDecimal(0.1)所創建的BigDecimal正好等於 0.1(非標度值 1,其標度爲 1),但是它實際上等於0.1000000000000000055511151231257827021181583404541015625。這是因爲0.1無法準確地表示爲 double(或者說對於該情況,不能表示爲任何有限長度的二進制小數)。這樣,傳入到構造方法的值不會正好等於 0.1(雖然表面上等於該值)。2、另一方面,String 構造方法是完全可預知的:寫入 newBigDecimal(“0.1”) 將創建一個 BigDecimal,它正好等於預期的 0.1。因此,比較而言,通常建議優先使用String構造方法。
BigDecimal加減乘除運算
public BigDecimal add(BigDecimal value); //加法
public BigDecimal subtract(BigDecimal value); //減法
public BigDecimal multiply(BigDecimal value); //乘法
public BigDecimal divide(BigDecimal value); //除法
除法的時候一定要注意,當出現不能整除的情況會會報錯java.lang.ArithmeticException: Non-terminatingdecimal expansion; no exact representable decimal result.其實divide方法有可以傳三個參數:public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) 第一參數表示除數, 第二個參數表示小數點後保留位數,第三個參數表示舍入模式。
roundingMode舍入模式
舍入模式和scale配合使用,其中scale是保留小數點後面的位數,而roundingMode是表示如何進行舍入,舍入模式有八種,下面將爲介紹。
UP // 舍入模式來遠離零。
5.5 6
2.5 3
1.6 2
1.1 2
1.0 1
-1.0 -1
-1.1 -2
-1.6 -2
-2.5 -3
-5.5 -6
DOWN // 舍入模式爲零.
5.5 5
2.5 2
1.6 1
1.1 1
1.0 1
-1.0 -1
-1.1 -1
-1.6 -1
-2.5 -2
-5.5 -5
CEILING// 舍入模式正無窮(我們常用的四捨五入);
5.5 6
2.5 3
1.6 2
1.1 2
1.0 1
-1.0 -1
-1.1 -1
-1.6 -1
-2.5 -2
-5.5 -5
FLOOR// 舍入模式向負無窮
5.5 5
2.5 2
1.6 1
1.1 1
1.0 1
-1.0 -1
-1.1 -2
-1.6 -2
-2.5 -3
-5.5 -6
HALF_UP// 四捨五入
5.5 6
2.5 3
1.6 2
1.1 2
1.0 1
-1.0 -1
-1.1 -1
-1.6 -2
-2.5 -3
-5.5 -6
HALF_DOWN// 五舍六入
5.5 5
2.5 2
1.6 2
1.1 1
1.0 1
-1.0 -1
-1.1 -1
-1.6 -2
-2.5 -2
-5.5 -5
HALF_EVEN// 當=.5的時候曏者偶數靠近
5.5 6
2.5 2
1.6 2
1.1 1
1.0 1
-1.0 -1
-1.1 -1
-1.6 -2
-2.5 -2
-5.5 -6
UNNECESSARY// 不允許需要舍入的,否則拋出異常:ArithmeticException
其他常用方法
// 比較兩個數的大小:
// -1, 0, or 1 as this BigDecimal is numerically less than, equal to, or greater than val
public int compareTo(BigDecimal val);