BigDecimal使用時遇到的問題

最近查看rebate數據時,發現一個bug,主要現象是,當扣款支付寶的賬號款項時,返回的是數字的金額爲元,而數據庫把金額存儲爲分,這中間要做元與分的轉化,這個轉化規則很簡單,就是*100的,所以一開始代碼很簡單,如下。

 


 
  1. Float f = Float.valueOf(s);

  2. f =f*100;

  3. Long result = f.longValue();

當s=”9.86”時,杯具出現了,result的結果爲985而不是986,float的精度損失導致float(985.99994)轉化爲整形時,丟掉小數部分成爲985,簡單的方法,我們可以提高精度使用雙精度的double類型,提高精度,比如


 
  1. Double d = Double.valueOf(s);

  2. d = d*100;

  3. Long result = d.longValue();

當s=”9.86”時,確實能夠得到正確結果,但是當s=”1219.86”時,這時候由於精度問題導致最終的result爲121985爲不是121986。當時以爲使用double解決的問題,其實隱藏更隱蔽的bug。
針對這樣的問題,如果使用C/C++語言,那麼通用解決方案可以這樣。

 


 
  1. Double d = Double.valueOf(s);

  2. d = d*100+0.5;// 注意這裏,我們使用的是+0.5的形式。

  3. Long result = d.longValue();

但是,我們使用的java語言,java應該有更優雅的解決方案,那就是BigDecimal。

使用BigDecimal的解決方案成這個樣子


 
  1. Double dd= Double.valueOf(s);

  2. BigDecimal bigD = new BigDecimal(dd);

  3. bigD = bigD.multiply(new BigDecimal(100));

  4. Long result = bigD.longValue();

狂暈,輸出結果是985爲不是986,打印bigD 

System.out.println(bigD.toString());

輸出如下

985.9999999999999431565811391919851303100585937500

不會再加上一個BigDecimal(0.5)吧。我相信在使用過BigDecimal過程中,肯定有那裏不對的地方,multiply方法中可以傳入精度,那就構造MathContext對象,修改如下。

 


 
  1. Double dd= Double.valueOf(s);

  2. BigDecimal bigD = new BigDecimal(dd);

  3. MathContextmc = new MathContext(4,RoundingMode.HALF_UP);

  4. //4表示取四位有效數字,RoundingMode.HALF_UP表示四捨五入

  5. bigD= bigD.multiply(new BigDecimal(100),mc);

  6. Long result = bigD.longValue();

最後結果輸出爲986,貌似已經找到完成解決方案,其實不然,注意到MathContext中的4了嘛?這是因爲我們保留4位有效數字,假如我們輸入的數字是大於4的,比如1219.86,最終輸出結果是122000,這是因爲1219.86保留4位有效數字時,第四位的9四捨五入,除去精確位補零,所以最終結果成了122000。問題就成了,我們必須知道元變分後的最終有效位數,”9.86”,有效位數是4,”19.86”有效位數是5,把字符串s的長度傳過去就可以了,那麼代碼如下


 
  1. Double dd =Double.valueOf(s);

  2. BigDecimalbigD = new BigDecimal(dd);

  3. MathContextmc = new MathContext(s.length(),RoundingMode.HALF_UP);

  4. //4表示取四位有效數字,RoundingMode.HALF_UP表示四捨五入

  5. bigD= bigD.multiply(new BigDecimal(100),mc);

  6. Long result = bigD.longValue();

至此,已經可以得到一個正確的元轉分的代碼,但是這裏的s.length()終歸不讓人感覺舒服,接下來,我們探索BigDecimal原理,嘗試用更優雅的方法解決這個問題。
BigDecimal,不可變的、任意精度的有符號十進制數。BigDecimal 由任意精度的整數非標度值 和 32 位的整數標度(scale) 組成。如果爲零或正數,則標度是小數點後的位數。如果爲負數,則將該數的非標度值乘以 10 的負 scale 次冪。因此,BigDecimal 表示的數值是 (unscaledValue × 10-scale)。我們知道BigDecimal有三個主要的構造函數

1

public BigDecimal(double val)

將double表示形式轉換爲BigDecimal

2

public BigDecimal(int val)

將int表示形式轉換爲BigDecimal

3

public BigDecimal(String val)

將字符串表示形式轉換爲BigDecimal

通過這三個構造函數,可以把double類型,int類型,String類型構造爲BigDecimal對象,在BigDecimal對象內通過BigIntegerintVal存儲傳遞對象數字部分,通過int scale;記錄小數點位數,通過int precision;記錄有效位數(默認爲0)。
BigDecimal的加減乘除就成了BigInteger與BigInteger之間的加減乘除,浮點數的計算也轉化爲整形的計算,可以大大提供性能,並且通過BigInteger可以保存大數字,從而實現真正大十進制的計算,在整個計算過程中,還涉及scale的判斷和precision判斷從而確定最終輸出結果。
我們先看一個例子


 
  1. BigDecimal d1 = new BigDecimal(0.6);

  2. BigDecimal d2 = new BigDecimal(0.4);

  3. BigDecimal d3 = d1.divide(d2);

  4. System.out.println(d3);

大家猜一下,以上輸出結果是?再接着看下面的代碼


 
  1. BigDecimal d1 = new BigDecimal(“0.6”);

  2. BigDecimal d2 = new BigDecimal(“0.4”);

  3. BigDecimal d3 = d1.divide(d2);

  4. System.out.println(d3);

看似相似的代碼,其結果完全不同,第一個例子中,拋出異常。第二個例子中,輸出打印結果爲1.5。造成這種差異的主要原因是第一個例子中的創建BigDecimal時,0.6和0.4是浮動類型的,浮點型放入BigDecimal內,其存儲值爲


 
  1. 0.59999999999999997779553950749686919152736663818359375

  2. 0.40000000000000002220446049250313080847263336181640625

這兩個浮點數相除時,由於除不盡,而又沒有設置精度和保留小數點位數,導致拋出異常。而第二個例子中0.6和0.4是字符串類型,由於BigDecimal存儲特性,通過BigInteger記錄BigDecimal的值,所以,0.6和0.4可以非常正確的記錄爲


 
  1. 0.6

  2. 0.4

兩者相除得出1.5來。
對於第一個例子,如果我們想得到正確結果,可以這樣來


 
  1. BigDecimal d1 = new BigDecimal(0.6);

  2. BigDecimal d2 = new BigDecimal(0.4);

  3. BigDecimal d3 = d1.divide(d2, 1, BigDecimal.ROUND_HALF_UP);

現在看我們留下的那個問題,使用更優雅的方式解決元轉化爲分的方式,上一個問題中,我們通過傳遞s.length()從而獲得精度,如果之前的s是double類型的,那邊這樣的方式就會有問題,通過上面的例子,我們可以調整爲一下的通用方式


 
  1. Double dd= Double.valueOf(s);

  2. BigDecimal bigD = new BigDecimal(dd);

  3. bigD = bigD.multiply(newBigDecimal(100)). divide(1, 1, BigDecimal.ROUND_HALF_UP);

/*這種方式也可以的

*d1 = new BigDecimal(s); d2= new BigDecimal("1"); d3=d1.multiply(d2); System.out.println(d3);

*/

 4.Long result = bigD.longValue();

我們通過/1,然後設置保留小數點方式,以及設置數字保留模式,從而得到兩個數乘積的小數部分。還有以下模式

枚舉常量摘要  
ROUND_CEILING   
          向正無限大方向舍入的舍入模式。 
ROUND_DOWN   
          向零方向舍入的舍入模式。 
ROUND_FLOOR   
          向負無限大方向舍入的舍入模式。 
ROUND_HALF_DOWN   
          向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向下舍入。 
ROUND_HALF_EVEN   
          向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向相鄰的偶數舍入。
ROUND_HALF_UP   
          向最接近數字方向舍入的舍入模式,如果與兩個相鄰數字的距離相等,則向上舍入。 
ROUND_UNNECESSARY   
          用於斷言請求的操作具有精確結果的舍入模式,因此不需要舍入。(默認模式) 
ROUND_UP   
          遠離零方向舍入的舍入模式。

總結:
1:儘量避免傳遞double類型,有可能話,儘量使用int和String類型。
2:做乘除計算時,一定要設置精度和保留小數點位數。
3:BigDecimal計算時,單獨放到try catch內。

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