Java BigDecimal 高精度商業運算實戰
浮點數 float/double 遵循 IEEE 754 標準,無法精確表示十進制小數 (如 0.1),導致商業運算 (金額、利率、稅費) 出現誤差。java.math.BigDecimal 提供任意精度有理數運算,是金融、電商、計費系統的標準選擇。本文系統講解建構器陷阱、核心運算、比較、格式化、效能優化與最佳實踐。
建構器陷阱:double vs String
BigDecimal d1 = new BigDecimal(0.1);
// 實際值: 0.1000000000000000055511151231257827021181583404541015625
BigDecimal d2 = new BigDecimal("0.1");
// 精確值: 0.1
- double 建構器:將 double 的二進制近似值直接轉換,引入不可預測誤差
- String 建構器:解析十進制字串,精確無誤,**強烈推薦**
- valueOf 工廠方法:
BigDecimal.valueOf(double) 內部用 Double.toString() 轉字串再解析,相對安全
核心運算 API
| 運算 | 方法 | 說明 |
| 加法 | add(BigDecimal) | 回傳新物件,不可變 |
| 減法 | subtract(BigDecimal) | |
| 乘法 | multiply(BigDecimal) | |
| 除法 | divide(BigDecimal, scale, roundingMode) | 必須指定精度與捨入模式,否則無限循環小數拋 ArithmeticException |
| 次方 | pow(int) | 冪次運算 |
| 開根號 | sqrt(MathContext) | JDK 9+ |
除法精度控制關鍵
// 錯誤:10 / 3 無限循環小數 → ArithmeticException
BigDecimal wrong = BigDecimal.TEN.divide(BigDecimal.valueOf(3));
// 正確:指定保留 2 位小數,四捨五入
BigDecimal correct = BigDecimal.TEN.divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP);
// 結果: 3.33
// 推薦:使用 MathContext 統一精度上下文
MathContext mc = new MathContext(10, RoundingMode.HALF_UP); // 10 位有效數字
BigDecimal result = BigDecimal.TEN.divide(BigDecimal.valueOf(3), mc);
// 結果: 3.333333333
比較與等值判斷
BigDecimal a = new BigDecimal("1.00");
BigDecimal b = new BigDecimal("1.0");
// compareTo: 比較數值大小,忽略尺度 (scale)
a.compareTo(b); // 0 (相等)
// equals: 比較數值 + 尺度 (1.00 vs 1.0 → false)
a.equals(b); // false
// 實務建議:永遠用 compareTo 判斷數值相等
if (a.compareTo(b) == 0) { ... }
型別轉換
| 來源 | 目標 | 方法 | 注意事項 |
| int/long | BigDecimal | new BigDecimal(val) / valueOf(val) | 精確轉換 |
| float/double | BigDecimal | BigDecimal.valueOf(val) | 避免 new BigDecimal(double) |
| String | BigDecimal | new BigDecimal(str) | 可能拋 NumberFormatException |
| BigDecimal | int/long | intValue() / longValue() | 截斷小數部份,不四捨五入 |
| BigDecimal | float/double | floatValue() / doubleValue() | 可能損失精度 |
| BigDecimal | String | toString() / toPlainString() | toPlainString 無科學記號 |
實戰範例:李白買酒問題 (逆向運算)
/*
李白遇店酒量翻倍,遇花飲一斗,五次後喝光。
逆向推導:第5次飲酒前有1斗 → 遇店前0.5斗 → 第4次飲酒前1.5斗 → 遇店前0.75斗...
*/
import java.math.*;
public class LiBaiWine {
public static void main(String[] args) {
BigDecimal volume = BigDecimal.ZERO; // 最後喝光 = 0
BigDecimal oneDou = BigDecimal.ONE; // 1斗
BigDecimal two = BigDecimal.valueOf(2);
for (int i = 0; i < 5; i++) {
// 逆向:先加回飲掉的1斗,再除以2 (遇店翻倍的逆運算)
volume = volume.add(oneDou).divide(two, 10, RoundingMode.HALF_UP);
}
System.out.println("初始酒量: " + volume + " 斗");
// 輸出: 0.96875 斗
}
}
金融運算最佳實踐清單
- 只用 String/valueOf 建構:禁用
new BigDecimal(double)
- 除法必指定精度與捨入:
divide(divisor, scale, RoundingMode.HALF_UP)
- 統一使用 MathContext:全域精度策略,避免各處硬編碼
- 比較用 compareTo:勿用 equals
- 貨幣單位用最小單位 (分/厘):儲存長整數,顯示時除以 100,完全避免小數點問題
- 不可變性:所有運算回傳新物件,鏈式調用
a.add(b).multiply(c)
- 序列化:實作 Serializable,但建議儲存為字串或長整數 (分)
- 資料庫映射:MySQL
DECIMAL(19,4)、PostgreSQL NUMERIC、JPA @Column(precision=19, scale=4)
效能優化提示
- 重用
MathContext、RoundingMode 常數
- 避免循環中重複建構相同常數 (預先定義
BigDecimal ONE = BigDecimal.ONE)
- 大量運算考慮
double 近似計算 + 關鍵步驟 BigDecimal 校驗
- 極高效能需求可用 JBigDecimal 或原生
long (分為單位)