首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

BigDecimal 精度陷阱:double 建構子導致 0.5 四捨五入失效的根因與最佳解法

     編輯:Java 綜合教學

現象重現

double d = 301353.05;
BigDecimal bd = new BigDecimal(d);
System.out.println(bd); // 301353.0499999999883584678173065185546875
System.out.println(bd.setScale(1, RoundingMode.HALF_UP)); // 301353.0 ❌ 預期 301353.1

經典的「0.5 四捨五入失效」實為 double → BigDecimal 建構子精度損失 導致。

根因深度剖析

IEEE 754 double 無法精確表示十進位小數

301353.05 在二進位是無限循環小數,double 只能存最接近的近似值:

301353.05 (十進位)
= 301353.0499999999883584678173065185546875 (double 實際儲存值)

BigDecimal(double) 建構子行為

JDK 文檔明確說明:new BigDecimal(double) 會將 double 的完整二進位精確值轉為 BigDecimal,而非人類可讀的十進位字面值。因此得到超長尾數的 BigDecimal,導致後續四捨五入基於錯誤基數。

三種解法優劣比較

✅ 方案一:字串建構子(推薦、最安全)

// 方式 1:直接字面值
BigDecimal bd1 = new BigDecimal("301353.05");

// 方式 2:Double.toString() 取得人類可讀最短表示
BigDecimal bd2 = new BigDecimal(Double.toString(d)); // "301353.05"

// 方式 3:String.valueOf(d) 同理
BigDecimal bd3 = new BigDecimal(String.valueOf(d));
  • 優點:零精度損失、效能佳、程式碼簡潔。
  • 適用:所有從 double/float 來源轉 BigDecimal 的場景。

⚠️ 方案二:valueOf 靜態工廠(JDK 內部同方案一)

BigDecimal bd = BigDecimal.valueOf(d); // 內部呼叫 Double.toString(d)
  • 優點:語義清晰、可快取常用值、同方案一安全。
  • 首選工廠方法。

🛠 方案三:自訂「無損轉換」工具(教學/特殊需求)

原理:將小數點後有效位數 n 計算,乘 10^n 轉長整數,再除回:

public static BigDecimal doubleToBigDecimal(double d) {
    String s = Double.toString(d); // 關鍵:取最短十進位表示
    int idx = s.indexOf('.');
    if (idx < 0) return new BigDecimal(s); // 整數
    int scale = s.length() - idx - 1;
    long unscaled = (long) (d * Math.pow(10, scale) + 0.5 * Math.signum(d));
    return new BigDecimal(unscaled, scale);
}
  • 缺點:d * pow 仍屬 double 運算,超過 15-16 位有效數字時會再次丟失精度(文中實測 14 位以上失效)。
  • 結論:不如方案一/二通用,僅供理解原理。

實測對比表

輸入 doubleString 建構子valueOfdouble 建構子自訂工具
301353.05301353.05 ✅301353.05 ✅301353.04999... ❌301353.05 ✅
301353.895898895455898954895989301353.895898895455898954895989 ✅301353.895898895455898954895989 ✅301353.89589889545459... ❌301353.89589889544 ❌
301353.46899434301353.46899434 ✅301353.46899434 ✅301353.46899433998... ❌301353.46899434 ✅

最佳實踐清單

  1. 嚴禁 使用 new BigDecimal(double) / new BigDecimal(float)。
  2. 優先用 BigDecimal.valueOf(double) 或 new BigDecimal(String)。
  3. 資料庫讀取 DECIMAL/NUMERIC 欄位時,用 ResultSet.getBigDecimal() 直接取得,避免經過 double。
  4. JSON 反序列化(Jackson/Gson)配置 DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS / GsonBuilder.setObjectTypeStrategy 直接產出 BigDecimal。
  5. 金融/計費系統全鏈路以 BigDecimal 字串流轉,杜絕 float/double。

總結

這不是 BigDecimal 的 Bug,而是 IEEE 754 double 本身精度限制 與 建構子語義誤解 的組合。牢記「字串進、字串出、valueOf 中轉」,精度問題將不再困擾。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved