BigDecimal setScale()設置無效 scale()取得的值不是setScale()設置的值


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

 

  1. Float f =  Float.valueOf(s);  
  2. f =f*100;  
  3. Long result = f.longValue();  
Float f =  Float.valueOf(s);
f =f*100;
Long result = f.longValue();

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

  1. Double d =  Double.valueOf(s);  
  2. d = d*100;  
  3. Long result = d.longValue();  
Double d =  Double.valueOf(s);
d = d*100;
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();  
Double d =  Double.valueOf(s);
d = d*100+0.5;// 注意這里,我們使用的是+0.5的形式。
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();  
Double dd= Double.valueOf(s);
BigDecimal bigD = new BigDecimal(dd);
bigD = bigD.multiply(new BigDecimal(100));
Long result = bigD.longValue();

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

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

輸出如下

  1. 985.9999999999999431565811391919851303100585937500  
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();  
Double dd= Double.valueOf(s);
BigDecimal bigD = new BigDecimal(dd);
MathContextmc = new MathContext(4,RoundingMode.HALF_UP);
//4表示取四位有效數字,RoundingMode.HALF_UP表示四舍五入
bigD= bigD.multiply(new BigDecimal(100),mc);
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();  
Double dd =Double.valueOf(s);
BigDecimalbigD = new BigDecimal(dd);
MathContextmc = new MathContext(s.length(),RoundingMode.HALF_UP);
//4表示取四位有效數字,RoundingMode.HALF_UP表示四舍五入
bigD= bigD.multiply(new BigDecimal(100),mc);
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);  
BigDecimal d1 = new BigDecimal(0.6);
BigDecimal d2 = new BigDecimal(0.4);
BigDecimal d3 = d1.divide(d2);
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);  
BigDecimal d1 = new BigDecimal(“0.6”);
BigDecimal d2 = new BigDecimal(“0.4”);
BigDecimal d3 = d1.divide(d2);
System.out.println(d3);

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

  1. 0.59999999999999997779553950749686919152736663818359375  
  2. 0.40000000000000002220446049250313080847263336181640625  
0.59999999999999997779553950749686919152736663818359375
0.40000000000000002220446049250313080847263336181640625

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

  1. 0.6  
  2. 0.4  
0.6
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);  
BigDecimal d1 = new BigDecimal(0.6);
BigDecimal d2 = new BigDecimal(0.4);
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);  
  4. Long result = bigD.longValue();  
Double dd= Double.valueOf(s);
BigDecimal bigD = new BigDecimal(dd);
bigD = bigD.multiply(newBigDecimal(100)). divide(1, 1, BigDecimal.ROUND_HALF_UP);
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類型,有可能話,盡量使用intString類型。 2:做乘除計算時,一定要設置精度和保留小數點位數。 3BigDecimal計算時,單獨放到try catch內。

public static BigDecimal double2BigDecimal(double d, int scale){

  BigDecimal db = new BigDecimal();

  db.setScale(scale, BigDecimal.ROUND_HALF_UP);

  return db;

}

以上方法返回的BigDecimal.scale()方法並不是我指定的值,於是修改成以下方法解決:

public static BigDecimal double2BigDecimal(double d, int scale){

  BigDecimal db = new BigDecimal();

  return db.divide(new BigDecimal(1), scale, BigDecimal.ROUND_HALF_UP);  ;

}

 

 

參考資料 IEEE 754簡介: http://baike.baidu.com/view/1698149.htm IEEE 754官方協議:http://grouper.ieee.org/groups/754/ BigDecimal函數列表:http://hi.baidu.com/logan9999/item/eeaea014677323fd9c778abd 浮點數與IEEE 754: http://www.cnblogs.com/kingwolfofsky/archive/2011/07/21/2112299.html MathContext:http://doc.java.sun.com/DocWeb/api/all/java.math.MathContext BigDecimal:http://doc.java.sun.com/DocWeb/api/all/java.math.BigDecimal


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM