關於BigDecimal的一個問題總結

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);

參考文獻

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章