1. 程式人生 > >BigDecimal使用2--保留小數點位數

BigDecimal使用2--保留小數點位數

轉載自逸學堂BigDecimal 的那些坑事兒

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

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

當s=”9.86”時,確實能夠得到正確結果,但是當s=”1219.86”時,這時候由於精度問題導致最終的result為121985為不是121986。當時以為使用double解決的問題,其實隱藏更隱蔽的bug。

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

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

System.out.println(bigD.toString());
輸出: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();

最後結果輸出為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();
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);

看似相似的程式碼,其結果完全不同,第一個例子中,丟擲異常。第二個例子中,輸出列印結果為1.5。造成這種差異的主要原因是第一個例子中的建立BigDecimal時,0.6和0.4是浮動型別的,浮點型放入BigDecimal內,其儲存值為

0.59999999999999997779553950749686919152736663818359375
0.40000000000000002220446049250313080847263336181640625

這兩個浮點數相除時,由於除不盡,而又沒有設定精度和保留小數點位數,導致丟擲異常。而第二個例子中0.6和0.4是字串型別,由於BigDecimal儲存特性,通過BigInteger記錄BigDecimal的值,所以,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);

現在看我們留下的那個問題,使用更優雅的方式解決元轉化為分的方式,上一個問題中,我們通過傳遞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();

我們通過/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內。