本文首發於個人微信公衆號《andyqian》,期待你的關注~
前言
在Java中,我們通常使用 BigDecimal 類型來表示金額,特別是在金融,財務系統中,使用的特別多。例如:轉賬金額,手續費等等。今天就一起來認識下BigDecimal。
爲什麼是BigDecimal ?
在此之前,我們先來講講爲什麼要使用 BigDecimal ?而不是Float,Double類型?其實光從表現形式來看,Float,Double,BigDecimal 類型都能表示小數。其區別在於精確計算時,Float 與 Double 類型都會損失精度,當然了,BigDecimal 使用不正確時,也會損失精度。在金融系統中,金額計算是最基本的運算,精度的丟失是絕對不能容忍的。接下來,我們來看看下面的例子:
Float 類型:
public void testFloat(){
float a = 1.1f;
float b = 0.8f;
System.out.println("a-b = "+(a-b));
System.out.println("a+b = "+(a+b));
System.out.println("a*b = "+(a*b));
System.out.println("a/b = "+(a/b));
}
結果如下:
a-b = 0.3
a+b = 1.9000001
a*b = 0.88000005
a/b = 1.375
Double 類型:
public void testDouble(){
double a = 1.1;
double b = 0.8;
System.out.println("a-b = "+(a-b));
System.out.println("a+b = "+(a+b));
System.out.println("a*b = "+(a*b));
System.out.println("a/b = "+(a/b));
}
結果如下:
a-b = 0.30000000000000004
a+b = 1.9000000000000001
a*b = 0.8800000000000001
a/b = 1.375
BigDecmial 錯誤使用:
public void testBigDecimal(){
BigDecimal a = new BigDecimal(1.1);
BigDecimal b = new BigDecimal(0.8);
System.out.println("a-b = "+(a.subtract(b)));
System.out.println("a+b = "+(a.add(b)));
System.out.println("a*b = "+(a.multiply(b)));
System.out.println("a/b = "+(a.divide(b)));
}
結果如下:
a-b = 0.3000000000000000444089209850062616169452667236328125
a+b = 1.9000000000000001332267629550187848508358001708984375
a*b = 0.8800000000000001199040866595169103100567462588676208086428264139311483660321755451150238513946533203125
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
正確使用方法:
public void testBigDecimalNormal(){
BigDecimal a = new BigDecimal("1.1");
BigDecimal b = new BigDecimal("0.8");
System.out.println("a-b = "+(a.subtract(b)));
System.out.println("a+b = "+(a.add(b)));
System.out.println("a*b = "+(a.multiply(b)));
System.out.println("a/b = "+(a.divide(b)));
}
結果如下:
a-b = 0.3
a+b = 1.9
a*b = 0.88
a/b = 1.375
通過上面的例子,我們可以清晰的看出。除了正確使用BigDecimal類型外,其餘的在計算過程中,均損失精度。因此我們可以得出以下結論:
- 在需要精度計算數值時,不應該使用float,double 類型,進行計算。
- BigDecimal 應該使用 String 構造函數,禁止使用double構造函數。
使用細節
其實,在使用BigDecimal過程,也有許多需要注意的細節。
- 科學計數法問題
@Test
public void testBigDecimalResult(){
BigDecimal b = new BigDecimal("0.0000001");
System.out.println(b.toString());
System.out.println(b.toPlainString());
}
執行結果:
1E-7
0.0000001
結論:當 BigDecimal的值 小於一定值時(測試時發現:小於等於0.0000001)時,則會被記爲科學計數法。可以使用 toPlainString()
方法顯示原來的值。
2. 去除多餘的 0
@Test
public void testBigDecimalStripZeros(){
BigDecimal b = new BigDecimal("0.000000100000000");
System.out.println(b.stripTrailingZeros().toString());
System.out.println(b.stripTrailingZeros().toPlainString());
}
使用場景:去除多餘的0,當金額有小數位限制時,使用該方法能夠去除掉無效的0,從而達到自動修復無效參數的目的。
結論:stripTrailingZeros() 方法的本質是去除掉多餘的0,其返回數據類型是BigDecimal,同樣的在使用時需要注意科學技術法的問題。
3. 保留小數位
@Test
public void testBigDecimalStripZeros(){
BigDecimal d = new BigDecimal("1.2222");
d.setScale(2);
System.out.println(d.toPlainString());
}
運行結果:
java.lang.ArithmeticException: Rounding necessary
原因:在setScale()方法中的roundingMode屬性設置爲了ROUND_UNNECESSARY,代碼如下:
public BigDecimal setScale(int newScale) {
return setScale(newScale,);
}
而在:
java.math.BigDecimal.commonNeedIncrement(BigDecimal.java:4179)
中ROUND_UNNECESSARY 類型恰恰會拋出異常。代碼顯示如下:
private static boolean commonNeedIncrement(int roundingMode, int qsign,
int cmpFracHalf, boolean oddQuot) {
switch(roundingMode) {
case ROUND_UNNECESSARY:
throw new ArithmeticException("Rounding necessary");
case ROUND_UP: // Away from zero
return true;
...
小結
通過上面的例子,現在我們已經知道了BigDecimal的一些使用細節。其實呀,這些都是血淋淋的教訓換來的經驗,每一個小細節對應的都是一個個事故,記憶猶新。這裏推薦大家都抽時間看看《Java開發手冊》,就能避免掉很多坑。
上面的問題,在《Java開發手冊》中同樣有寫到:
【強制】爲了防止精度損失,禁止使用構造方法BigDecimal(double)的方式把double值轉化爲BigDecimal對象。
說明:BigDecimal(double) 存在精度損失風險,在精確計算或值比較的場景中可能會導致業務邏輯異常。如:BigDecimal g = new BigDecimal(0.1f); 實際的存儲值爲:0.10000000149
正例:優先推薦入參爲String 的構造函數,或使用BigDecimal的valueOf方法。
相關閱讀: