Java 中 “精度計算有誤” 是高頻問題,核心根源是浮點數(float/double)的二進制存儲特性—— 十進制小數(如 0.1)無法被二進制精確表示,導致加減乘除時出現精度丟失;此外,商業計算(金額、税率)若誤用浮點數,還會放大誤差。
一、先搞懂:為什麼會精度丟失?
直觀例子(運行以下代碼就能看到問題):
java
運行
public class PrecisionTest {
public static void main(String[] args) {
// 看似簡單的加法,結果卻不是 0.3
double a = 0.1;
double b = 0.2;
System.out.println(a + b); // 輸出:0.30000000000000004
// 金額計算誤差更明顯
double money1 = 1.0;
double money2 = 0.99;
System.out.println(money1 - money2); // 輸出:0.010000000000000009
}
}
二、常見精度丟失場景 & 原因
|
場景
|
錯誤示例
|
誤差表現
|
根因
|
|
小數加減
|
|
結果≈0.30000000000000004
|
十進制小數轉二進制無限循環,近似存儲
|
|
金額乘除
|
|
結果≈6.000000000000001
|
浮點數運算時舍入誤差累積
|
|
浮點數轉整數
|
|
結果 = 2(預期 = 3)
|
二進制存儲的近似值小於 3,強轉截斷
|
|
高精度數值運算
|
|
結果丟失小數位
|
double 有效精度僅 15-17 位,超出後丟失
|
三、解決方案(按優先級排序)
方案 1:商業計算首選 → BigDecimal(精準控制精度)
核心使用規則(避坑!):
正確示例(解決 0.1+0.2 問題):
java
運行
import java.math.BigDecimal;
import java.math.RoundingMode;
public class BigDecimalTest {
public static void main(String[] args) {
// 正確構造:用 String 或 valueOf
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = BigDecimal.valueOf(0.2); // 內部會轉成精準的字符串
// 1. 加法
BigDecimal addResult = a.add(b);
System.out.println("0.1+0.2 = " + addResult); // 輸出:0.3
// 2. 減法(金額計算示例)
BigDecimal money1 = new BigDecimal("1.00");
BigDecimal money2 = new BigDecimal("0.99");
BigDecimal subResult = money1.subtract(money2);
System.out.println("1.00-0.99 = " + subResult); // 輸出:0.01
// 3. 乘法(税率計算:1000元 * 6%)
BigDecimal amount = new BigDecimal("1000");
BigDecimal taxRate = new BigDecimal("0.06");
BigDecimal tax = amount.multiply(taxRate);
System.out.println("1000*0.06 = " + tax); // 輸出:60.00
// 4. 除法(必須指定舍入模式,否則除不盡拋異常)
BigDecimal c = new BigDecimal("10");
BigDecimal d = new BigDecimal("3");
// 保留2位小數,四捨五入
BigDecimal divResult = c.divide(d, 2, RoundingMode.HALF_UP);
System.out.println("10/3 = " + divResult); // 輸出:3.33
}
}
常用舍入模式(按需選擇):
|
舍入模式
|
含義
|
適用場景
|
|
|
四捨五入(最常用)
|
金額、税率計算
|
|
|
銀行家舍入(四捨六入五取偶)
|
金融場景(減少累計誤差)
|
|
|
截斷(直接捨棄小數位)
|
庫存計數(只舍不入)
|
|
|
向上取整(進一法)
|
運費計算(只要有小數就進 1)
|
方案 2:整數換算(適合簡單金額計算)
java
運行
public class IntegerCalcTest {
public static void main(String[] args) {
// 1.00元 = 100分,0.99元 = 99分
long money1 = 100; // 分
long money2 = 99; // 分
long diff = money1 - money2; // 1分
// 轉回元:除以100
System.out.println("1.00-0.99 = " + diff / 100.0); // 輸出:0.01
}
}
方案 3:浮點數運算後修正(僅臨時補救,不推薦)
java
運行
public class DoubleFixTest {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
double sum = a + b;
// 保留1位小數,四捨五入
double fixedSum = Math.round(sum * 10) / 10.0;
System.out.println(fixedSum); // 輸出:0.3
}
}
四、高頻避坑點
BigDecimal x = new BigDecimal("0.3");
BigDecimal y = new BigDecimal("0.30");
System.out.println(x == y); // false(地址不同)
System.out.println(x.compareTo(y) == 0); // true(值相等)
BigDecimal num = new BigDecimal("123.45");
double d = num.doubleValue(); // 123.45
long l = num.setScale(0, RoundingMode.HALF_UP).longValue(); // 123
五、最佳實踐總結
|
場景
|
推薦方案
|
禁止方案
|
|
金額 / 税率 / 金融計算
|
BigDecimal(String 構造)+ 指定舍入模式
|
float/double 直接運算
|
|
簡單固定小數位計算
|
整數換算(分 / 釐為單位)
|
double 運算後四捨五入
|
|
科學計算 / 非精度敏感場景
|
double
|
BigDecimal(沒必要,浪費性能)
|