如何使用BigDecimal實現Java開發商業計算
前言
今天群裡一個初級開發者問為什麼測試人員測出來他寫的價格計算模組有計算偏差的問題,他檢查了半天也沒找出問題。這裡小胖哥要提醒你,商業計算請務必使用BigDecimal,浮點做商業運算是不精確的。因為計算機無法使用二進位制小數來精確描述我們程式中的十進位制小數。《Effective Java》在第48條也推薦“使用BigDecimal來做精確運算”。今天我們就來總結歸納其相關的知識點。
BigDecimal
BigDecimal表示不可變的任意精度帶符號十進位制數。它由兩部分組成:
- intVal - 未校正精度的整數,型別為BigInteger
- Scale - 一個32位整數,表示小數點右邊的位數
例如,BigDecimal 3.14的未校正值為314,縮放為2。我們使用BigDecimal進行高精度算術運算。我們還將它用於需要控制比例和舍入行為的計算。如果你的計算是商業計算請務必使用計算精確的BigDecimal 。
構造BigDecimal例項
我們可以從String,character 陣列,int,long和BigInteger建立一個BigDecimal物件:
@Test public void theValueMatches() { BigDecimal bdFromString = new BigDecimal("0.12"); BigDecimal bdFromCharArray = new BigDecimal(new char[]{'3','.','1','4','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 或者直接使用其未校正數加小數位數 :
@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方法來建構函式。
常用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 |
BigDecimal操作
BigDecimal上的操作就像其他Number類(Integer,Long,Double等)一樣,BigDecimal提供算術和比較操作的操作。它還提供了縮放操作,舍入和格式轉換的操作。它不會使算術運算子(+ - /*)或邏輯運算子(> < | &) 過載。相反,我們使用BigDecimal相應的方法 - 加,減,乘,除和比較。並且BigDecimal具有提取各種屬性的方法。
提取屬性
精度,小數位數和符號:
@Test public void whenGettingAttributes_thenExpectedResult() { BigDecimal bd = new BigDecimal("-12345.6789"); assertEquals(9,bd.precision()); assertEquals(4,bd.scale()); assertEquals(-1,bd.signum()); }
比較大小
我們使用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方法:
@Test public void whenEqualsCalled_thenSizeAndScaleMatched() { BigDecimal bd1 = new BigDecimal("1.0"); BigDecimal bd2 = new BigDecimal("1.00"); assertFalse(bd1.equals(bd2)); }
四則運算
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); }
四捨五入
既然是數學運算就不得不講四捨五入。比如我們在金額計算中很容易遇到最終結算金額為人民幣22.355的情況。因為貨幣沒有比分更低的單位所以我們要使用精度和舍入模式規則對數字進行剪裁。java提供有兩個類控制舍入行為RoundingMode和MathContext 。MathContext執行的是IEEE 754R標準目前不太明白其使用場景,我們使用的比較多的是列舉RoundingMode。它提供了八種模式:
RoundingMode.UP:以小數位為原點 是正數取右邊,負數取左邊
RoundingMode.DOWN:以小數位為原點 也就是正數取左邊,負數取右邊
RoundingMode.FLOOR:取左邊最近的正數
RoundingMode.CEILING:取右邊最近的整數
RoundingMode.HALF_DOWN:五舍六入,負數先取絕對值再五舍六入再負數
RoundingMode.HALF_UP:四捨五入,負數原理同上
RoundingMode.HALF_EVEN:這個比較繞,整數位若是奇數則四捨五入,若是偶數則五舍六入
RoundingMode.ROUND_UNNECESSARY:不需要取整,如果存在小數位,就拋ArithmeticException 異常
格式化
數字格式化可通過操作類java.text.NumberFormat和java.text.DecimalFormat提供的api進行操作。其實我們只需要使用java.text.DecimalFormat,因為它代理了NumberFormat。我們來看一下它們的api:
NumberFormat
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。
DecimalFormat
DecimalFormat除了能代理上面的NumberFormat以外,還提供了基於pattern字串的格式化風格,有點類似格式化時間一樣。我們來看看pattern的規則:
- “0”——表示一位數值,如沒有,顯示0。如“0000.0000”,整數位或小數位>4,按實際輸出,<4整數位前面補0小數位後面補0,湊足4位。
- “#”——表示任意位數的整數。如沒有,則不顯示。在小數點位使用,只表示一位小數,超出部分四捨五入。如:“#”:無小數,小數部分四捨五入。“.#”:整數部分不變,一位小數,四捨五入。“.##”:整數部分不變,二位小數,四捨五入。
- “.”——表示小數點。注意一個pattern中只能出現一次,超過一次將格式化異常。
- “,”——與模式“0”一起使用,表示逗號。注意一定不能在小數點後用,否則格式化異常。
總結
今天對BigDecimal進行了總結歸納,這篇文章建議你收藏備用,也可以轉給其他需要的同學。
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。