BigDecimal
這篇文章我們會介紹一下Java 中的BigDecimal,並且會通過一些例子演示它的用法,例如精度的操作
Java在java.math包中提供的API類BigDecimal,用來對超過16位有效位的數進行精確的運算。雙精度浮點型變量double可以處理16位有效數,但在實際應用中,可能需要對更大或者更小的數進行運算和處理。一般情況下,對於那些不需要準確計算精度的數字,我們可以直接使用Float和Double處理,但是Double.valueOf(String) 和Float.valueOf(String)會丟失精度。所以開發中,如果我們需要精確計算的結果,則必須使用BigDecimal類來操作。
為什麼需要BigDecimal
前面學習基本類型的時候,我們可以使用float 和 double來表示浮點型數字,但是這裡有一個問題那就是基本數據類型float 和 double不應該用於高精度數據的表示,例如貨幣,因為浮點類型的float 和 double會丟失數據的精度
double d1 = 374.56;
double d2 = 374.26;
System.out.println( "d1 - d2 = " + ( d1 - d2 ));
這裡本應輸出的是0.30,但是實際上輸出如下
d1 - d2 = 0.30000000000001137
這就是為什麼在金融相關的程序里數據類型如此重要的原因,所以我們更好的選擇是BigDecimal 而不是float 和 double
Java BigDecimal 類
BigDecimal 是不可變的,任意精度的有符號十進制類型的數字,可用於貨幣計算
上面那個例子如果我們是使用BigDecimal來代替double 我們可以獲得準確的值
BigDecimal bd1 = new BigDecimal("374.56");
BigDecimal bd2 = new BigDecimal("374.26");
System.out.println("bd1 - bd2 = " + bd1.subtract(bd2));
現在輸出就和預期的一樣了
bd1 - bd2 = 0.30
Java 中的BigDecimal類繼承自Number並且實現了Comparable接口
public class BigDecimal extends Number implements Comparable<BigDecimal> {
}
BigDecimal 類的構造方法
BigDecimal 提供了很多的構造方法,可以使用int ,char[],BigDecimal,String,doble ,long,int來初始化BigDecimal,BigDecimal 總共提供了18種構造方法,需要注意的實如果使用double 來初始化BigDecimal或許會再次引入精度的問題,下面提供了一個例子
BigDecimal bde = new BigDecimal(23.12);
System.out.println("" + bde.toString());
輸出結果是這樣的
23.120000000000000994759830064140260219573974609375
Thus it is always safe to go with a constructor that takes String as argument when representing a decimal value.
因此使用String 做為構造函數的參數來表示一個十進制的數字的時候總是安全的
BigDecimal bde = new BigDecimal("23.12");
System.out.println("" + bde.toString());
Output
23.12
BigDecimal 的精度
使用BigDecimal的一個理由是BigDecimal提供了精度控制(小數點後的數字的多少)和舍入模式,為了確定小數點後的保留幾位數字你可以使用setScale(int scale) 方法,但是最好的是在使用精度的時候提供舍入模式,也就是setScale的重載方法
- setScale(int newScale, int roundingMode)
- setScale(int newScale, RoundingMode roundingMode)
接下來我們通過一個例子演示一下我們為什麼要這樣做,假設我們在通過一個double值構造一個BigDecimal
BigDecimal bde = new BigDecimal(23.12);
System.out.println("Value- " + bde.toString());
System.out.println("Scaled value- " + bde.setScale(1).toString());
Output
Value- 23.120000000000000994759830064140260219573974609375
Exception in thread "main" java.lang.ArithmeticException: Rounding necessary
at java.base/java.math.BigDecimal.commonNeedIncrement(BigDecimal.java:4495)
at java.base/java.math.BigDecimal.needIncrement(BigDecimal.java:4702)
at java.base/java.math.BigDecimal.divideAndRound(BigDecimal.java:4677)
at java.base/java.math.BigDecimal.setScale(BigDecimal.java:2811)
at java.base/java.math.BigDecimal.setScale(BigDecimal.java:2853)
at org.netjs.Programs.App.main(App.java:15)
從上面的輸出中我們看到進度已經丟失了,輸出的BigDecimal 是
23.120000000000000994759830064140260219573974609375
並且我們看到當我們將精度設置為1 的時候並且沒有提供舍入機制的時候導致Arithmetic異常被拋出
BigDecimal 的舍入模式
如果你注意到了上面我們在講精度設置的時候,它其實是有兩個設置精度的重載方法,第二個參數代表的就是舍入模式模式的參數,BigDecimal提供了八種舍入模式,它們通過static final int 進行表示
public final static int ROUND_UP = 0;
public final static int ROUND_DOWN = 1;
public final static int ROUND_CEILING = 2;
public final static int ROUND_FLOOR = 3;
public final static int ROUND_HALF_UP = 4;
public final static int ROUND_HALF_DOWN = 5;
public final static int ROUND_HALF_EVEN = 6;
public final static int ROUND_UNNECESSARY = 7;
需要注意的是在java.math包中也提供舍入模式的枚舉值,需要注意的我們是推薦使用枚舉值來代替使用int 類型的常量做舍入摸模式的參數
下面我們在設置進度的同時設置一下舍入模式,來避免Arithmetic異常
@Test
public void scale() {
BigDecimal bde = new BigDecimal(23.12);
System.out.println("Scaled value- " + bde.setScale(1,1).toString());
}
但是我們說了,我們推薦使用枚舉值的舍入模式,而不是直接使用int 類型的常量,接下來我們看一下RoundingMode 提供的枚舉值
- CEILING– Rounding mode to round towards positive infinity.
- DOWN– Rounding mode to round towards zero.
- FLOOR– Rounding mode to round towards negative infinity.
- HALF_DOWN– Rounding mode to round towards “nearest neighbor” unless both neighbors are equidistant, in which case round down.
- HALF_EVEN– Rounding mode to round towards the “nearest neighbor” unless both neighbors are equidistant, in which case, round towards the even neighbor.
- HALF_UP– Rounding mode to round towards “nearest neighbor” unless both neighbors are equidistant, in which case round up.
- UNNECESSARY – Rounding mode to assert that the requested operation has an exact result, hence no rounding is necessary.
- UP– Rounding mode to round away from zero.
下面我們通過例子來總結一下這些舍入模式的舍入方式
Result of rounding input to one digit with the given rounding mode | ||||||||
Input Number | UP | DOWN | CEILING | FLOOR | HALF_UP | HALF_DOWN | HALF_EVEN | UNNECESSARY |
5.5 | 6 | 5 | 6 | 5 | 6 | 5 | 6 | throw ArithmeticException |
2.5 | 3 | 2 | 3 | 2 | 3 | 2 | 2 | throw ArithmeticException |
1.6 | 2 | 1 | 2 | 1 | 2 | 2 | 2 | throw ArithmeticException |
1.1 | 2 | 1 | 2 | 1 | 1 | 1 | 1 | throw ArithmeticException |
1.0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
-1.0 | -1 | -1 | -1 | -1 | -1 | -1 | -1 | -1 |
-1.1 | -2 | -1 | -1 | -2 | -1 | -1 | -1 | throw ArithmeticException |
-1.6 | -2 | -1 | -1 | -2 | -2 | -2 | -2 | throw ArithmeticException |
-2.5 | -3 | -2 | -2 | -3 | -3 | -2 | -2 | throw ArithmeticException |
-5.5 | -6 | -5 | -5 | -6 | -6 | -5 | -6 | throw ArithmeticException |
更多細節參考–
docs.oracle.com/javase/8/do…
BigDecimal格式化
NumberFormat 格式化
由於NumberFormat類的format()方法可以使用BigDecimal對象作為其參數,可以利用BigDecimal對超出16位有效數字的貨幣值,百分值,以及一般數值進行格式化控制。
以利用BigDecimal對貨幣和百分比格式化為例。首先,創建BigDecimal對象,進行BigDecimal的算術運算後,分別建立對貨幣和百分比格式化的引用,最後利用BigDecimal對象作為format()方法的參數,輸出其格式化的貨幣值和百分比。
@Test
public void format() {
NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立貨幣格式化引用
NumberFormat percent = NumberFormat.getPercentInstance(); //建立百分比格式化引用
percent.setMaximumFractionDigits(3); //百分比小數點最多3位
BigDecimal loanAmount = new BigDecimal("15000.48"); //貸款金額
BigDecimal interestRate = new BigDecimal("0.008"); //利率
BigDecimal interest = loanAmount.multiply(interestRate); //相乘
System.out.println("貸款金額:t" + currency.format(loanAmount));
System.out.println("利率:t" + percent.format(interestRate));
System.out.println("利息:t" + currency.format(interest));
}
輸出結果
貸款金額: ¥15,000.48
利率: 0.8%
利息: ¥120.00
DecimalFormat 格式化
BigDecimal格式化保留2為小數,不足則補0
@Test
public void format2(){
DecimalFormat df = new DecimalFormat("#.00");
System.out.println(formatToNumber(new BigDecimal("3.435")));
System.out.println(formatToNumber(new BigDecimal(0)));
System.out.println(formatToNumber(new BigDecimal("0.00")));
System.out.println(formatToNumber(new BigDecimal("0.001")));
System.out.println(formatToNumber(new BigDecimal("0.006")));
System.out.println(formatToNumber(new BigDecimal("0.206")));
}
/**
* @desc
* 1. 0~1之間的BigDecimal小數,格式化後失去前面的0,則前面直接加上0。
* 2. 傳入的參數等於0,則直接返回字符串"0.00"
* 3. 大於1的小數,直接格式化返回字符串
* @return
*/
public String formatToNumber(BigDecimal obj) {
DecimalFormat df = new DecimalFormat("#.00");
if(obj.compareTo(BigDecimal.ZERO)==0) {
return "0.00";
}else if(obj.compareTo(BigDecimal.ZERO)>0&&obj.compareTo(new BigDecimal(1))<0){
return "0"+df.format(obj).toString();
}else {
return df.format(obj).toString();
}
}
BigDecimal 例子
最常見的例子就是精度是2(小數點後保留兩位),並且採用四捨五入的舍入模式(如果指定精度的笑一個數字大於等於5則向上取整,否則向下取整)
@Test
public void examples() {
BigDecimal bd1 = new BigDecimal("23.126");
System.out.println("bd1 " + bd1.setScale(2, RoundingMode.HALF_UP).toString());
}
// 輸出結果
bd1 23.13
因為精度設置為2之後,也就是小數點後兩位的後一位數字是6大於等於5,所以向上取整,所以結果是 23.13
BigDecimal bd1 = new BigDecimal("23.1236");
System.out.println("bd1 " + bd1.setScale(2, RoundingMode.HALF_UP).toString());
因為精度設置為2之後,也就是小數點後兩位的後一位數字是3小於5,所以向下取整,所以結果是 23.12
BigDecimal bd1 = new BigDecimal("-15.567");
System.out.println("bd1 " + bd1.setScale(2, RoundingMode.HALF_UP).toString());
對於負數,也是同樣的道理,所以輸出是bd1 -15.57
BigDecimal 的特性
1. 沒有重載操作符
在Java 中支持的(+, -, *, /)數學運算,BigDecimal並不支持,因為這些操作符是針對基本數據類型的,但是BigDecimal是引用類型,也就是基於對象和類的,因此BigDecimal提供了下面的方法
add,subtract,multiply,and,divide
例如乘法在BigDecimal的實現如下
BigDecimal bd1 = new BigDecimal("15.567");
BigDecimal result = BigDecimal.valueOf(68).multiply(bd1);
System.out.println("result " + result.toString());
Output
result 1058.556
2. 使用 compareTo() 來比較BigDecimals 而不是使用 equals()
需要注意如果你使用 equals()來比較兩個BigDecimal數字,那只有當兩個BigDecimal的值和精度都相同的時候equals()猜認為它們是相同的(因此2.0和2.00是不相同的)
BigDecimal bd1 = new BigDecimal("2.00");
BigDecimal bd2 = new BigDecimal("2.0");
System.out.println("bd1 equals bd2 - " + bd1.equals(bd2));
Output
bd1 equals bd2 - false
因此你應該使用compareTo()方法來比較兩個BigDecimal 是否是相等的,BigDecimal實現了comparable接口並且提供了自己的compareTo方法,這個方法只會判斷兩個BigDecimal對象的值是否是相等的忽略了兩個數字的精度(ike 2.0 和 2.00 相等的)
對於bd1.compareTo(bd2) 的返回值
- -1 bd1 小於 bd2.
- 0 兩個相等的
- 1 bd1 大於 bd2.
BigDecimal bd1 = new BigDecimal("2.00");
BigDecimal bd2 = new BigDecimal("2.0");
System.out.println("bd1 compareTo bd2 - " + bd1.compareTo(bd2));
Output
bd1 compareTo bd2 - 0
3. BigDecimals 是不可變的
BigDecimal 對象是不可變的,所以是線程安全的,在進行每一次四則運算時,都會產生一個新的對象 ,所以在做加減乘除運算時要記得要保存操作後的值。
總結
- 當我們在進行有着高精度的計算要求的時候不要使用double和float 因為它們有着精度丟失的問題
- 如果使用BigDecimal的時候,不要選擇double值作為初始化的值,因為它同樣會引入精度的問題
- 如果你使用BigDecimal時候設置了精度,那就同時提供舍入模式,告訴BigDecimal如何舍入從而提供你想要的精度
- BigDecimal繼承了Number類和實現了Comparable接口
- BigDecimal 針對加減乘除提供可特定的方法,因為BigDecimal不支持(+, -, *, /)數學運算
- BigDecimal 的對象是不可變的
- BigDecimal因為創建對象開銷的原因,所以很多操作都是比原生類型要慢一些的。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/252515.html