Java開發中商業計算請務必使用BigDecimal來進行計算!

前言

今天羣裏一個初級開發者問爲何測試人員測出來他寫的價格計算模塊有計算誤差的問題,他檢查了半天也沒找出問題。這裏小胖哥要提醒你,商業計算請務必使用BigDecimal,浮點作商業運算是不精確的。由於計算機沒法使用二進制小數來精確描述咱們程序中的十進制小數。《Effective Java》在第48條也推薦「使用BigDecimal來作精確運算」。今天咱們就來總結概括其相關的知識點。java

1. BigDecimal

BigDecimal表示不可變的任意精度帶符號十進制數。它由兩部分組成:git

  • intVal - 未校訂精度的整數,類型爲BigInteger
  • Scale - 一個32位整數,表示小數點右邊的位數 例如,BigDecimal 3.14的未校訂值爲314,縮放爲2。咱們使用BigDecimal進行高精度算術運算。咱們還將它用於須要控制比例和舍入行爲的計算。若是你的計算是商業計算請務必使用計算精確的BigDecimal

2. 構造BigDecimal

咱們能夠從Stringcharacter 數組,intlongBigInteger建立一個BigDecimal對象:api

@Test
    public void theValueMatches() {

        BigDecimal bdFromString = new BigDecimal("0.12");
        BigDecimal bdFromCharArray = new BigDecimal(new char[]{'3', '.', '1', '4', '1', '5'});
        BigDecimal bdlFromInt = new BigDecimal(42);
        BigDecimal bdFromLong = new BigDecimal(123412345678901L);
        BigInteger bigInteger = BigInteger.probablePrime(100, new Random());
        BigDecimal bdFromBigInteger = new BigDecimal(bigInteger);

        assertEquals("0.12", bdFromString.toString());
        assertEquals("3.1415", bdFromCharArray.toString());
        assertEquals("42", bdlFromInt.toString());
        assertEquals("123412345678901", bdFromLong.toString());
        assertEquals(bigInteger.toString(), bdFromBigInteger.toString());

    }

咱們還能夠從double建立BigDecimal數組

@Test
public void whenBigDecimalCreatedFromDouble_thenValueMayNotMatch() {
    BigDecimal bdFromDouble = new BigDecimal(0.1d);
    assertNotEquals("0.1", bdFromDouble.toString());
}

咱們發如今這種狀況下,結果與預期的結果不一樣(即0.1)。這是由於:這個轉換結果是double的二進制浮點值的精確十進制表示,其值得結果不是咱們能夠預測的.咱們應該使用String構造函數而不是double構造函數。另外,咱們可使用valueOf靜態方法將double轉換爲BigDecimal 或者直接使用其未校訂數加小數位數 :dom

@Test
    public void whenBigDecimalCreatedUsingValueOf_thenValueMatches() {

        BigDecimal bdFromDouble = BigDecimal.valueOf(0.1d);
        BigDecimal  bigFromLong=BigDecimal.valueOf(1,1);

        assertEquals("0.1", bdFromDouble.toString());
        assertEquals("0.1", bigFromLong.toString());
    }

在轉換爲BigDecimal以前,此方法將double轉換爲其String表示形式。此外,它能夠重用對象實例。所以,咱們應該優先使用valueOf方法來構造函數。ide

3. 經常使用API

方法名 對應方法相關用法解釋
abs() 絕對值,scale不變
add(BigDecimal augend) 加,scale爲augend和原值scale的較大值
subtract(BigDecimal augend) 減,scale爲augend和原值scale的較大值
multiply(BigDecimal multiplicand) 乘,scale爲augend和原值scale的和
divide(BigDecimal divisor) 除,原值/divisor,若是不能除盡會拋出異常,scale與原值一致
divide(BigDecimal divisor, int roundingMode) 除,指定舍入方式,scale與原值一致
divide(BigDecimal divisor, int scale, int roundingMode) 除,指定舍入方式和scale
remainder(BigDecimal divisor) 取餘,scale與原值一致
divideAndRemainder(BigDecimal divisor) 除法運算後返回一個數組存放除盡和餘數 如 23/3 返回 {7,2}
divideToIntegralValue(BigDecimal divisor) 除,只保留整數部分,但scale仍與原值一致
max(BigDecimal val) 較大值,返回原值與val中的較大值,與結果的scale一致
min(BigDecimal val) 較小值,與結果的scale一致
movePointLeft(int n) 小數點左移,scale爲原值scale+n
movePointRight(int n) 小數點右移,scale爲原值scale+n
negate() 取反,scale不變
pow(int n) 冪,原值^n,原值的n次冪
scaleByPowerOfTen(int n) 至關於小數點右移n位,原值*10^n

4. BigDecimal操做

BigDecimal上的操做就像其餘Number類(Integer,Long,Double等)同樣,BigDecimal提供算術和比較操做的操做。它還提供了縮放操做,舍入和格式轉換的操做。它不會使算術運算符+, - ,/,*或邏輯運算符>、< 、|、& 過載。相反,咱們使用BigDecimal相應的方法 - 加,減,乘,除和比較。而且BigDecimal具備提取各類屬性的方法。函數

4.1 提取屬性

精度,小數位數和符號:測試

@Test
public void whenGettingAttributes_thenExpectedResult() {
    BigDecimal bd = new BigDecimal("-12345.6789");
         
    assertEquals(9, bd.precision());
    assertEquals(4, bd.scale());
    assertEquals(-1, bd.signum());
}

4.2 比較大小

咱們使用compareTo方法比較兩個BigDecimal的值:代理

@Test
public void whenComparingBigDecimals_thenExpectedResult() {
    BigDecimal bd1 = new BigDecimal("1.0");
    BigDecimal bd2 = new BigDecimal("1.00");
    BigDecimal bd3 = new BigDecimal("2.0");
 
    assertTrue(bd1.compareTo(bd3) < 0);
    assertTrue(bd3.compareTo(bd1) > 0);
    assertTrue(bd1.compareTo(bd2) == 0);
    assertTrue(bd1.compareTo(bd3) <= 0);
    assertTrue(bd1.compareTo(bd2) >= 0);
    assertTrue(bd1.compareTo(bd3) != 0);
}

上面的方法在比較時忽略了小數位。若是你既要比較精度又要比較小數位數那麼請使用equals方法:code

@Test
public void whenEqualsCalled_thenSizeAndScaleMatched() {
    BigDecimal bd1 = new BigDecimal("1.0");
    BigDecimal bd2 = new BigDecimal("1.00");
         
    assertFalse(bd1.equals(bd2));
}

4.3 四則運算

BigDecimal 提供瞭如下四則運算的方法:

  • add ——加法
  • subtract ——減法
  • divide ——除法,有可能除不盡,必須顯式聲明保留小數位數避免拋出ArithmeticException異常
  • multiply ——乘法
@Test
public void whenPerformingArithmetic_thenExpectedResult() {
    BigDecimal bd1 = new BigDecimal("4.0");
    BigDecimal bd2 = new BigDecimal("2.0");
 
    BigDecimal sum = bd1.add(bd2);
    BigDecimal difference = bd1.subtract(bd2);
    BigDecimal quotient = bd1.divide(bd2);
    BigDecimal product = bd1.multiply(bd2);
 
    assertTrue(sum.compareTo(new BigDecimal("6.0")) == 0);
    assertTrue(difference.compareTo(new BigDecimal("2.0")) == 0);
    assertTrue(quotient.compareTo(new BigDecimal("2.0")) == 0);
    assertTrue(product.compareTo(new BigDecimal("8.0")) == 0);
}

4.4 四捨五入

既然是數學運算就不得不講四捨五入。好比咱們在金額計算中很容易遇到最終結算金額爲人民幣22.355的狀況。由於貨幣沒有比分更低的單位因此咱們要使用精度和舍入模式規則對數字進行剪裁。java提供有兩個類控制舍入行爲RoundingModeMathContextMathContext執行的是IEEE 754R標準目前不太明白其使用場景,咱們使用的比較多的是枚舉RoundingMode。它提供了八種模式:

  • RoundingMode.UP:以小數位爲原點 是正數取右邊,負數取左邊
  • RoundingMode.DOWN:以小數位爲原點 也就是正數取左邊,負數取右邊
  • RoundingMode.FLOOR:取左邊最近的正數
  • RoundingMode.CEILING:取右邊最近的整數
  • RoundingMode.HALF_DOWN:五舍六入,負數先取絕對值再五舍六入再負數
  • RoundingMode.HALF_UP:四捨五入,負數原理同上
  • RoundingMode.HALF_EVEN:這個比較繞,整數位如果奇數則四捨五入,如果偶數則五舍六入
  • RoundingMode.ROUND_UNNECESSARY:不須要取整,若是存在小數位,就拋ArithmeticException 異常

5. 格式化

數字格式化可經過操做類java.text.NumberFormatjava.text.DecimalFormat提供的api進行操做。其實咱們只須要使用java.text.DecimalFormat,由於它代理了NumberFormat。咱們來看一下它們的api:

5.1 NumberFormat

  • getInstance(Locale)、getNumberInstance(Locale)。返回指定語言環境的通用數值格式。
  • NumberFormat.getCurrencyInstance(Locale)。 返回指定語言環境的貨幣格式。
  • NumberFormat.getPercentInstance(Locale)。 返回指定語言環境的百分比格式。
  • NumberFormat.getIntegerInstance(Locale)。 返回指定語言環境的整數數值格式。
  • NumberFormat.setMinimumIntegerDigits(int)。設置數的整數部分所容許的最小位數。
  • NumberFormat.setMaximumIntegerDigits(int)。設置數的整數部分所容許的最大位數。
  • NumberFormat.setMinimumFractionDigits(int)。設置最少小數點位數,不足的位數以0補位,超出的話按實際位數輸出。
  • NumberFormat.setMaximumFractionDigits(int)。設置最多保留小數位數,不足不補0。

5.2 DecimalFormat

DecimalFormat除了能代理上面的NumberFormat之外,還提供了基於pattern字符串的格式化風格,有點相似格式化時間同樣。咱們來看看pattern的規則:

  • 「0」——表示一位數值,如沒有,顯示0。如「0000.0000」,整數位或小數位>4,按實際輸出,<4整數位前面補0小數位後面補0,湊足4位。

  • 「#」——表示任意位數的整數。如沒有,則不顯示。在小數點位使用,只表示一位小數,超出部分四捨五入。如:「#」:無小數,小數部分四捨五入。「.#」:整數部分不變,一位小數,四捨五入。「.##」:整數部分不變,二位小數,四捨五入。

  • 「.」——表示小數點。注意一個pattern中只能出現一次,超過一次將格式化異常。

  • 「,」——與模式「0」一塊兒使用,表示逗號。注意必定不能在小數點後用,不然格式化異常。

總結

今天對BigDecimal進行了總結概括,這篇文章建議你收藏備用,也能夠轉給其餘須要的同窗。

關注公衆號:碼農小胖哥 獲取更多資訊

相關文章
相關標籤/搜索