最近查看rebate數據時,發現一個bug,主要現象是,當扣款支付寶的賬號款項時,返回的是數字的金額為元,而數據庫把金額存儲為分,這中間要做元與分的轉化,這個轉化規則很簡單,就是*100的,所以一開始代碼很簡單,如下。
- Float f = Float.valueOf(s);
- f =f*100;
- Long result = f.longValue();
Float f = Float.valueOf(s); f =f*100; Long result = f.longValue();
當s=”9.86”時,杯具出現了,result的結果為985而不是986,float的精度損失導致float(985.99994)轉化為整形時,丟掉小數部分成為985,簡單的方法,我們可以提高精度使用雙精度的double類型,提高精度,比如
- Double d = Double.valueOf(s);
- d = d*100;
- 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++語言,那么通用解決方案可以這樣。
- Double d = Double.valueOf(s);
- d = d*100+0.5;// 注意這里,我們使用的是+0.5的形式。
- Long result = d.longValue();
Double d = Double.valueOf(s); d = d*100+0.5;// 注意這里,我們使用的是+0.5的形式。 Long result = d.longValue();
但是,我們使用的java語言,java應該有更優雅的解決方案,那就是BigDecimal。
使用BigDecimal的解決方案成這個樣子
- Double dd= Double.valueOf(s);
- BigDecimal bigD = new BigDecimal(dd);
- bigD = bigD.multiply(new BigDecimal(100));
- 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
- System.out.println(bigD.toString());
System.out.println(bigD.toString());
輸出如下
- 985.9999999999999431565811391919851303100585937500
985.9999999999999431565811391919851303100585937500
不會再加上一個BigDecimal(0.5)吧。我相信在使用過BigDecimal過程中,肯定有那里不對的地方,multiply方法中可以傳入精度,那就構造MathContext對象,修改如下。
- 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();
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的長度傳過去就可以了,那么代碼如下
- 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();
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判斷從而確定最終輸出結果。 我們先看一個例子
- BigDecimal d1 = new BigDecimal(0.6);
- BigDecimal d2 = new BigDecimal(0.4);
- BigDecimal d3 = d1.divide(d2);
- 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);
大家猜一下,以上輸出結果是?再接着看下面的代碼
- BigDecimal d1 = new BigDecimal(“0.6”);
- BigDecimal d2 = new BigDecimal(“0.4”);
- BigDecimal d3 = d1.divide(d2);
- 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.6和0.4是浮動類型的,浮點型放入BigDecimal內,其存儲值為
- 0.59999999999999997779553950749686919152736663818359375
- 0.40000000000000002220446049250313080847263336181640625
0.59999999999999997779553950749686919152736663818359375 0.40000000000000002220446049250313080847263336181640625
這兩個浮點數相除時,由於除不盡,而又沒有設置精度和保留小數點位數,導致拋出異常。而第二個例子中0.6和0.4是字符串類型,由於BigDecimal存儲特性,通過BigInteger記錄BigDecimal的值,所以,0.6和0.4可以非常正確的記錄為
- 0.6
- 0.4
0.6 0.4
兩者相除得出1.5來。 對於第一個例子,如果我們想得到正確結果,可以這樣來
- BigDecimal d1 = new BigDecimal(0.6);
- BigDecimal d2 = new BigDecimal(0.4);
- 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類型的,那邊這樣的方式就會有問題,通過上面的例子,我們可以調整為一下的通用方式
- 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();
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類型,有可能話,盡量使用int和String類型。 2:做乘除計算時,一定要設置精度和保留小數點位數。 3:BigDecimal計算時,單獨放到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