Java除法不精確引入BigDecimal

前言:

之前在項目裏遇到一個問題:169/100=1.0

int nPtCount = 169;
final int MAX_ROUTE_PT = 100;
double lfDivide = 0;

if (nPtCount > MAX_ROUTE_PT) {

    lfDivide = nPtCount / MAX_ROUTE_PT;
    nPtCount = MAX_ROUTE_PT;
    Log.d("TraceList","航跡被劃分:"+lfDivide );
}

log打印出來是1.0,後來查閱資料,發現用BigDecimal除法才能精確。

BigDecimal 構造方法

  1. public BigDecimal(double val)    //將double表示形式轉換爲BigDecimal *不建議使用
  2. public BigDecimal(int val)  //將int表示形式轉換成BigDecimal
  3. public BigDecimal(String val)  //將String表示形式轉換成BigDecimal

爲什麼不建議採用第一種構造方法呢?來看例子

public static void main(String[] args)
    {
        BigDecimal bigDecimal = new BigDecimal(2);
        BigDecimal bDouble = new BigDecimal(2.3);
        BigDecimal bString = new BigDecimal("2.3");
        System.out.println("bigDecimal=" + bigDecimal);
        System.out.println("bDouble=" + bDouble);
        System.out.println("bString=" + bString);
    }

運行結果如下:

爲什麼會出現這種情況呢?

JDK的描述:

1、參數類型爲double的構造方法的結果有一定的不可預知性。有人可能認爲在Java中寫入newBigDecimal(0.1)所創建的BigDecimal正好等於 0.1(非標度值 1,其標度爲 1),但是它實際上等於0.1000000000000000055511151231257827021181583404541015625。這是因爲0.1無法準確地表示爲 double(或者說對於該情況,不能表示爲任何有限長度的二進制小數)。這樣,傳入到構造方法的值不會正好等於 0.1(雖然表面上等於該值)。

2、另一方面,String 構造方法是完全可預知的:寫入 newBigDecimal("0.1") 將創建一個 BigDecimal,它正好等於預期的 0.1。因此,比較而言,通常建議優先使用String構造方法

當double必須用作BigDecimal的源時,請使用Double.toString(double)轉成String,然後使用String構造方法,或使用BigDecimal的靜態方法valueOf,如下:

public static void main(String[] args)
    {
        BigDecimal bDouble1 = BigDecimal.valueOf(2.3);
        BigDecimal bDouble2 = new BigDecimal(Double.toString(2.3));

        System.out.println("bDouble1=" + bDouble1);
        System.out.println("bDouble2=" + bDouble2);
        
    }

結果如下:

BigDecimal加減乘除運算

對於常用的加,減,乘,除,BigDecimal類提供了相應的成員方法。

public BigDecimal add(BigDecimal value);                        //加法
public BigDecimal subtract(BigDecimal value);                   //減法 
public BigDecimal multiply(BigDecimal value);                   //乘法
public BigDecimal divide(BigDecimal value);                     //除法

大概的用法如下:

public static void main(String[] args)
    {
        BigDecimal a = new BigDecimal("4.5");
        BigDecimal b = new BigDecimal("1.5");

        System.out.println("a + b =" + a.add(b));
        System.out.println("a - b =" + a.subtract(b));
        System.out.println("a * b =" + a.multiply(b));
        System.out.println("a / b =" + a.divide(b));
    }

運行結果:

這裏有一點需要注意的是除法運算divide.

BigDecimal除法可能出現不能整除的情況,比如 4.5/1.3,這時會報錯java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

其實divide方法有可以傳三個參數

public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) 

第一參數表示除數, 二個參數表示小數點後保留位數,第三個參數表示舍入模式。

只有在作除法運算或四捨五入時纔用到舍入模式,有下面這幾種:

  • ROUND_UP :向遠離零的方向舍入。捨棄非零部分,並將非零捨棄部分相鄰的一位數字加一。
  • ROUND_DOWN :向接近零的方向舍入。捨棄非零部分,同時不會非零捨棄部分相鄰的一位數字加一,採取截取行爲。
  • ROUND_CEILING :向正無窮的方向舍入。如果爲正數,舍入結果同ROUND_UP一致;如果爲負數,舍入結果同ROUND_DOWN一致。注意:此模式不會減少數值大小。
  • ROUND_FLOOR :向負無窮的方向舍入。如果爲正數,舍入結果同ROUND_DOWN一致;如果爲負數,舍入結果同ROUND_UP一致。注意:此模式不會增加數值大小。
  • ROUND_HALF_UP :向“最接近”的數字舍入,如果與兩個相鄰數字的距離相等,則爲向上舍入的舍入模式。如果捨棄部分>= 0.5,則舍入行爲與ROUND_UP相同;否則舍入行爲與ROUND_DOWN相同。這種模式也就是我們常說的我們的“四捨五入”。
  • ROUND_HALF_DOWN :向“最接近”的數字舍入,如果與兩個相鄰數字的距離相等,則爲向下舍入的舍入模式。如果捨棄部分> 0.5,則舍入行爲與ROUND_UP相同;否則舍入行爲與ROUND_DOWN相同。這種模式也就是我們常說的我們的“五舍六入”。
  • ROUND_HALF_EVEN :向“最接近”的數字舍入,如果與兩個相鄰數字的距離相等,則相鄰的偶數舍入。如果捨棄部分左邊的數字奇數,則舍入行爲與 ROUND_HALF_UP 相同;如果爲偶數,則舍入行爲與 ROUND_HALF_DOWN 相同。注意:在重複進行一系列計算時,此舍入模式可以將累加錯誤減到最小。此舍入模式也稱爲“銀行家舍入法”,主要在美國使用。四捨六入,五分兩種情況,如果前一位爲奇數,則入位,否則捨去。
  • ROUND_UNNECESSARY :斷言請求的操作具有精確的結果,因此不需要舍入。如果對獲得精確結果的操作指定此舍入模式,則拋出ArithmeticException。

按照各自的需要,可傳入合適的第三個參數。四捨五入採用 ROUND_HALF_UP

需要對BigDecimal進行截斷和四捨五入可用setScale方法,例:

public static void main(String[] args)
    {
        BigDecimal a = new BigDecimal("4.5635");

        a = a.setScale(3, RoundingMode.HALF_UP);    //保留3位小數,且四捨五入
        System.out.println(a);
    }

減乘除其實最終都返回的是一個新的BigDecimal對象,因爲BigInteger與BigDecimal都是不可變的(immutable)的,在進行每一步運算時,都會產生一個新的對象:

public static void main(String[] args)
    {
        BigDecimal a = new BigDecimal("4.5");
        BigDecimal b = new BigDecimal("1.5");
        a.add(b);

       System.out.println(a);  //輸出4.5. 加減乘除方法會返回一個新的BigDecimal對象,原來的a不變
    }

BigDecimal比較大小

BigDecimal a = new BigDecimal (101);
BigDecimal b = new BigDecimal (111);
 
//使用compareTo方法比較
//注意:a、b均不能爲null,否則會報空指針
if(a.compareTo(b) == -1){
    System.out.println("a小於b");
}
 
if(a.compareTo(b) == 0){
    System.out.println("a等於b");
}
 
if(a.compareTo(b) == 1){
    System.out.println("a大於b");
}
 
if(a.compareTo(b) > -1){
    System.out.println("a大於等於b");
}
 
if(a.compareTo(b) < 1){
    System.out.println("a小於等於b");
}

BigDecimal轉String

public static void main(String[] args) {
        // 浮點數的打印
        System.out.println(new BigDecimal("10000000000").toString());

        // 普通的數字字符串
        System.out.println(new BigDecimal("100.000").toString());

        // 去除末尾多餘的0
        System.out.println(new BigDecimal("100.000").stripTrailingZeros().toString());

        // 避免輸出科學計數法
        System.out.println(new BigDecimal("100.000").stripTrailingZeros().toPlainString());

}

// output
100.000
1E+2
  1. 用toString()方法輸出的就是普通的數字字符串。
  2. stripTrailingZeros()函數就是用於去除末尾多餘的0的,
  3. 用toPlainString()函數代替toString(),避免輸出科學計數法的字符串。

總結

  1. 商業計算使用BigDecimal。
  2. 儘量使用參數類型爲String的構造函數。
  3. BigDecimal都是不可變的(immutable)的,在進行每一步運算時,都會產生一個新的對象,所以在做加減乘除運算時千萬要保存操作後的值。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章