StackTraces
Java已驗證

BigDecimal 除法報 Non-terminating decimal expansion

@debug_master32 天前更新閱讀 6 min00

這個例外不是 bug,是 BigDecimal 的「精確優先」設計:除法要返回精確結果,但 1 除以 3 這類結果小數位無限延伸,根本表示不出來,於是直接拋例外而不是悄悄給你一個近似值。修法不是換掉 BigDecimal,而是每次除法都把「保留幾位 + 怎麼舍入」說清楚。

Issue & Env(案發現場與環境)

  • 現象:金額、計價等用到 BigDecimal 的程式碼,運行到某次除法突然拋例外,接口 500。
  • 環境:Java 任何版本,java.math.BigDecimal,常見於財務/計費/庫存計算裡除不盡的情況。

StackTrace(堆疊日誌)

java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
    at java.math.BigDecimal.divide(BigDecimal.java:1736)
    at com.divudi.ejb.PharmacyCalculation.saveItemBatchWithCosting(PharmacyCalculation.java:786)
    at com.divudi.ejb.PharmacyCalculation.processBill(PharmacyCalculation.java:312)
    at com.divudi.jsf.PharmacyBillController.save(PharmacyBillController.java:124)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.lang.reflect.Method.invoke(Method.java:498)

一句「no exact representable decimal result」點明瞭本質:結果無法精確表示。

Reproduce(復現程式碼)

用單參數的 divide 做除法,結果除不盡:

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
System.out.println(a.divide(b)); // 抛异常

甚至 40 / 60 這種直覺上「除得盡」的例子也會炸第一步,因為 divide 要返回一個精確的十進制商,而 40 / 60 的十進制表示是循環小數 0.6666…,沒法用有限位精確寫下。

Root Cause(根因分析)

BigDecimal 用「無標度整數 + 標度(scale)」表示一個精確的十進制數。divide(divisor) 這個重載要求返回一個精確商,用的標度是「被除數標度減去除數標度」,且不允許舍入。一旦商的十進制展開是無限循環小數(1/3、10/7 之類),它無法在有限位裡精確表示,就只能拋 ArithmeticException。

所以報錯與否,取決於除法的結果是否「有限小數」,而不在於程式碼寫得對不對。add、subtract、multiply 都沒這個問題,唯獨除法會因為除不盡而踩坑。很多人在開發環境的數據恰好除得盡、沒炸,上線後換了一批訂單數據就崩了。

另一個易錯點是「只傳 RoundingMode 不傳 scale」:即便你指定了舍入模式,Java 仍需要一個明確的標度來決定保留幾位小數,缺少它同樣可能對除不盡的結果拋例外。

The Fix(解決方案)

除法統一寫成「三參數」:被除數、保留位數、舍入模式:

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
// 保留 2 位小数,四舍五入
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);

金額類計算建議把精度提取成常量統一管理,避免各寫各的造成舍入不一致:

private static final int MONEY_SCALE = 6;

BigDecimal perUnit = price.divide(units, MONEY_SCALE, RoundingMode.HALF_EVEN);

RoundingMode.HALF_EVEN(銀行家舍入)對金額累加比 HALF_UP 更不容易產生累積偏差,適合財務場景。如果只是要個整數結果,也仍然要傳 RoundingMode.HALF_UP,而不是依賴 divide 的預設行為。

需要精確到「無盡精度」的數學運算(不是貨幣)時,改用 MathContext.DECIMAL128 這類帶精度上限的上下文;但那已經是另一套語義,業務金額別這麼用。

References(參考來源)

最後更新於 2026-09-09

這篇幫到你了嗎?

剛解決了一個棘手的報錯?花兩分鐘記錄下來,幫助下一個遇到同樣問題的開發者。

貢獻一條解法