BigDecimal詳解和精度問題
一、背景
在實際開發中,對於 不需要任何准確計算精度的屬性可以直接使用float或double,但是如果需要精確計算結果,則必須使用BigDecimal,例如價格、質量。
為什么這么說,主要有兩點
1、double計算會有精度丟失問題
2、在除法運算時,BigDecimal提供了豐富的取舍規則。(double雖然可以通過NumberFormat進行四舍五入,但是NumberFormat是線程不安全的)
對於精度問題我們可以看下實際的例子
public static void main(String[] args) {
//正常 3.3
System.out.println("加法結果:"+(1.1+2.2));
//正常 -7.9
System.out.println("減法結果:"+(2.2-10.1));
//正常 2.42
System.out.println("乘法結果:"+(1.1*2.2));
//正常 0.44
System.out.println("除法結果:"+(4.4/10));
}
實際控制台輸出

為什么會這樣原因?
在於我們的計算機是二進制的。浮點數沒有辦法是用二進制進行精確表示。我們的CPU表示浮點數由兩個部分組成:指數和尾數
,這樣的表示方法一般都會
失去一定的精確度,有些浮點數運算也會產生一定的誤差。如:2.4的二進制表示並非就是精確的2.4。反而最為接近的二進制表示是 2.3999999999999999。
浮點數的值實際上是由一個特定的數學公式計算得到的。
二、BigDecimal構造函數
1、四種構造函數
BigDecimal(int) //創建一個具有參數所指定整數值的對象。
BigDecimal(double) //創建一個具有參數所指定雙精度值的對象。
BigDecimal(long) //創建一個具有參數所指定長整數值的對象。
BigDecimal(String) //創建一個具有參數所指定以字符串表示的數值的對象。
這幾個都是常用的構造器,他們返回的對象都是BigDecimal對象。換而言之,將BigDecimal對象轉換為其他類型的對象,我們通過以下幾種。
toString() //將BigDecimal對象的數值轉換成字符串。
doubleValue() //將BigDecimal對象中的值以雙精度數返回。
floatValue() //將BigDecimal對象中的值以單精度數返回。
longValue() //將BigDecimal對象中的值以長整數返回。
intValue() //將BigDecimal對象中的值以整數返回。
這里需要非常注意BigDecimal(double)的構造函數,也是會存在精度丟失的問題
,其它的不會,這里也可以舉例說明
public static void main(String[] args) {
BigDecimal intDecimal = new BigDecimal(10);
BigDecimal doubleDecimal = new BigDecimal(4.3);
BigDecimal longDecimal = new BigDecimal(10L);
BigDecimal stringDecimal = new BigDecimal("4.3");
System.out.println("intDecimal=" + intDecimal);
System.out.println("doubleDecimal=" + doubleDecimal);
System.out.println("longDecimal=" + longDecimal);
System.out.println("stringDecimal=" + stringDecimal);
}
控制台實際輸出

從圖中很明顯可以看出,對於double的構造函數是會存在精度丟失的可能的。
2、為什么會出現這種情況
這個在new BigDecimal(double)類型的構造函數上的注解有解釋說明。
這個構造函數的結果可能有些不可預測。 可以假設在Java中寫入new BigDecimal(0.1)創建一個BigDecimal ,它完全等於0.1(非標尺值為1,比例為1),但實際上等於
0.1000000000000000055511151231257827021181583404541015625。 這是因為0.1不能像double (或者作為任何有限長度的二進制分數)精確地表示。
因此,正在被傳遞給構造的值不是正好等於0.1。
3、如何解決
有兩種常用的解決辦法。
1、是將double 通過Double.toString(double)
先轉為String,然后放入BigDecimal的String構造函數中。
2、不通過BigDecimal的構造函數,而是通過它的靜態方法BigDecimal.valueOf(double)
,也同樣不會丟失精度。
示例
public static void main(String[] args) {
String string = Double.toString(4.3);
BigDecimal stringBigDecimal = new BigDecimal(string);
BigDecimal bigDecimal = BigDecimal.valueOf(4.3);
System.out.println("stringBigDecimal = " + stringBigDecimal);
System.out.println("bigDecimal = " + bigDecimal);
}
運行結果

這樣也能保證,對與double而言,轉BigDecimal不會出現精度丟失的情況。
三、常用方法
1、常用方法
示例
public static void main(String[] args) {
BigDecimal a = new BigDecimal("4.5");
BigDecimal b = new BigDecimal("1.5");
BigDecimal c = new BigDecimal("-10.5");
BigDecimal add_result = a.add(b);
BigDecimal subtract_result = a.subtract(b);
BigDecimal multiply_result = a.multiply(b);
BigDecimal divide_result = a.divide(b);
BigDecimal remainder_result = a.remainder(b);
BigDecimal max_result = a.max(b);
BigDecimal min_result = a.min(b);
BigDecimal abs_result = c.abs();
BigDecimal negate_result = a.negate();
System.out.println("4.5+1.5=" + add_result);
System.out.println("4.5-1.5=" + subtract_result);
System.out.println("4.5*1.5=" + multiply_result);
System.out.println("4.5/1.5=" + divide_result);
System.out.println("4.5/1.5余數=" + remainder_result);
System.out.println("4.5和1.5最大數=" + max_result);
System.out.println("4.5和1.5最小數=" + min_result);
System.out.println("-10.5的絕對值=" + abs_result);
System.out.println("4.5的相反數=" + negate_result);
}
運行結果
4.5+1.5=6.0
4.5-1.5=3.0
4.5*1.5=6.75
4.5/1.5=3
4.5/1.5余數=0.0
4.5和1.5最大數=4.5
4.5和1.5最小數=1.5
-10.5的絕對值=10.5
4.5的相反數=-4.5
這里把除法
單獨再講一下,因為除法操作的時候會有除不盡的情況,,比如 3,5/3,這時會報錯java.lang.ArithmeticException: Non-terminating decimal expansion;
no exact representable decimal result。所以這里要考慮除不盡的情況下,保留幾位小數,取舍規則。(除法如果可能存在除不進,那就用下面方法)
BigDecimal divide(BigDecimal divisor, int scale, int roundingMode) 第一參數表示除數,第二個參數表示小數點后保留位數,第三個參數表示取舍規則。
2、取舍規則
ROUND_UP //不管保留數字后面是大是小(0除外)都會進1
ROUND_DOWN //保留設置數字,后面所有直接去除
ROUND_HALF_UP //常用的四舍五入
ROUND_HALF_DOWN //五舍六入
ROUND_CEILING //向正無窮方向舍入
ROUND_FLOOR //向負無窮方向舍入
ROUND_HALF_EVEN //向(距離)最近的一邊舍入,除非兩邊(的距離)是相等,如果是這樣,如果保留位數是奇數,使用ROUND_HALF_UP,如果是偶數,使用ROUND_HALF_DOWN
ROUND_UNNECESSARY //計算結果是精確的,不需要舍入模式
注意
我們最常用的應該是 ROUND_HALF_UP(四舍五入)
上面這樣解釋還是有點模糊,具體可以看這篇文章,示例非常清楚 BigDecimal的四舍五入的RoundingMode 選擇
這里舉幾個常用的取舍規則
public static void main(String[] args) {
BigDecimal a = new BigDecimal("1.15");
BigDecimal b = new BigDecimal("1");
//不管保留數字后面是大是小(0除外)都會進1 所以這里輸出為1.2
BigDecimal divide_1 = a.divide(b,1,BigDecimal.ROUND_UP);
//保留設置數字,后面所有直接去除 所以這里輸出為1.1
BigDecimal divide_2 = a.divide(b,1,BigDecimal.ROUND_DOWN);
//常用的四舍五入 所以這里輸出1.2
BigDecimal divide_3 = a.divide(b,1,BigDecimal.ROUND_HALF_UP);
//這個可以理解成五舍六入 所以這里輸出1.1
BigDecimal divide_4 = a.divide(b,1,BigDecimal.ROUND_HALF_DOWN);
//這里將1.15改成1.16
BigDecimal c = new BigDecimal("1.16");
//那么這里就符合六入了 所以輸出變為1.2
BigDecimal divide_5 = c.divide(b,1,BigDecimal.ROUND_HALF_DOWN);
System.out.println("divide_1 = " + divide_1);
System.out.println("divide_2 = " + divide_2);
System.out.println("divide_3 = " + divide_3);
System.out.println("divide_4 = " + divide_4);
System.out.println("divide_5 = " + divide_5);
}
運行結果
divide_1 = 1.2
divide_2 = 1.1
divide_3 = 1.2
divide_4 = 1.1
divide_5 = 1.2
四、格式化
由於NumberFormat類的format()方法可以使用BigDecimal對象作為其參數,可以利用BigDecimal對超出16位有效數字的貨幣值,百分值,以及一般數值進行格式化控制。
以利用BigDecimal對貨幣和百分比格式化為例。首先,創建BigDecimal對象,進行BigDecimal的算術運算后,分別建立對貨幣和百分比格式化的引用,最后利用
BigDecimal對象作為format()方法的參數,輸出其格式化的貨幣值和百分比。
示例
public static void main(String[] args) {
//建立貨幣格式化引用
NumberFormat currency = NumberFormat.getCurrencyInstance();
//建立百分比格式化引用
NumberFormat percent = NumberFormat.getPercentInstance();
//百分比小數點最多3位
percent.setMaximumFractionDigits(3);
//取整
NumberFormat integerInstance = NumberFormat.getIntegerInstance();
////金額
BigDecimal loanAmount = new BigDecimal("188.555");
////利率
BigDecimal interestRate = new BigDecimal("0.018555555");
//沒有指定保留位數的情況下 默認保留2位
System.out.println("金額: " + currency.format(loanAmount));
//貨幣(百分比)格式化 指定默認的取舍規則是四舍五入
System.out.println("利率: " + percent.format(interestRate));
//取整還有點不一樣 188.555取整為189, 188.51也是189 但是189.5確實188,所以它不是真正意義上的四舍五入
System.out.println("取整: " + integerInstance.format(loanAmount));
}
運行結果
金額: ¥188.56
利率: 1.856%
取整: 189
這里有幾點在說明下
1、格式化的時候沒有指定保留位數的情況下 默認保留2位。
2、貨幣(百分比)格式化 指定默認的取舍規則是四舍五入。
3、取整還有點不一樣 188.555取整為189, 188.51也是189 但是189.5確實188,所以它不是真正意義上的四舍五入。
有關NumberFormat詳細介紹可以參考文章:NumberFormat--數字格式化類
別人罵我胖,我會生氣,因為我心里承認了我胖。別人說我矮,我就會覺得好笑,因為我心里知道我不可能矮。這就是我們為什么會對別人的攻擊生氣。
攻我盾者,乃我內心之矛(28)