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 建構子精度損失 導致。
301353.05 在二進位是無限循環小數,double 只能存最接近的近似值:
301353.05 (十進位)
= 301353.0499999999883584678173065185546875 (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));
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 位以上失效)。| 輸入 double | String 建構子 | valueOf | double 建構子 | 自訂工具 |
|---|---|---|---|---|
| 301353.05 | 301353.05 ✅ | 301353.05 ✅ | 301353.04999... ❌ | 301353.05 ✅ |
| 301353.895898895455898954895989 | 301353.895898895455898954895989 ✅ | 301353.895898895455898954895989 ✅ | 301353.89589889545459... ❌ | 301353.89589889544 ❌ |
| 301353.46899434 | 301353.46899434 ✅ | 301353.46899434 ✅ | 301353.46899433998... ❌ | 301353.46899434 ✅ |
new BigDecimal(double) / new BigDecimal(float)。BigDecimal.valueOf(double) 或 new BigDecimal(String)。DECIMAL/NUMERIC 欄位時,用 ResultSet.getBigDecimal() 直接取得,避免經過 double。DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS / GsonBuilder.setObjectTypeStrategy 直接產出 BigDecimal。這不是 BigDecimal 的 Bug,而是 IEEE 754 double 本身精度限制 與 建構子語義誤解 的組合。牢記「字串進、字串出、valueOf 中轉」,精度問題將不再困擾。