掌握 Java 中的 BigDecimal:精確的金額計算,杜絕浮點誤差

目次

1. 介紹

Java 中數值計算的精確度問題

在 Java 程式設計中,數值計算每天都會進行。例如,計算產品價格、確定稅金或利息 — 這些操作在許多應用程式中都是必要的。然而,當使用浮點數類型如 floatdouble 進行此類計算時,可能會發生意外的錯誤。 這是因為 floatdouble二進位近似值 表示數值。像「0.1」或「0.2」這樣在十進位中可以精確表示的值,在二進位中無法精確表示 — 因此,會累積小錯誤。

BigDecimal 對於貨幣或精確計算至關重要

此類錯誤在 貨幣計算和高精度科學/工程計算 等領域可能至關重要。例如,在計費計算中,即使是 1 日圓的差異也可能導致信譽問題。 這就是 Java 的 BigDecimal 類別的優勢所在。BigDecimal 可以 處理任意精度的十進位數字,並透過取代 floatdouble 使用它,可以無錯誤地進行數值計算。

閱讀本文您將獲得什麼

在本文中,我們將系統性地解釋 Java 中 BigDecimal 的基本用法、高級技巧,以及常見錯誤和注意事項。 這對於那些希望在 Java 中準確處理貨幣計算,或考慮在專案中採用 BigDecimal 的人很有用。

2. BigDecimal 是什麼?

BigDecimal 概覽

BigDecimal 是 Java 中的一個類別,能夠進行高精度十進位運算。它屬於 java.math 套件,並專門設計用於 不容許錯誤的計算,例如財務/會計/稅務計算。 使用 Java 的 floatdouble,數值以二進位近似值儲存 — 意味著像「0.1」或「0.2」這樣的十進位無法精確表示,這是錯誤的來源。相較之下,BigDecimal基於字串的十進位表示 儲存數值,從而抑制捨入和近似錯誤。

處理任意精度數字

BigDecimal 最大的特徵是「任意精度」。整數和小數部分理論上可以處理 幾乎無限的位數,避免因位數限制而導致的捨入或位數遺失。 例如,下列大數字可以精確處理:

BigDecimal bigValue = new BigDecimal("12345678901234567890.12345678901234567890");

能夠像這樣在保持精度的同時進行運算,是 BigDecimal 的主要優勢。

主要使用案例

建議在以下情況中使用 BigDecimal

  • 貨幣計算 — 財務應用程式中的利息、稅率計算
  • 發票 / 報價金額處理
  • 需要高精度的科學/工程計算
  • 長期累積導致錯誤累積的過程

例如,在 會計系統和薪資計算 中 — 1 日圓的差異可能導致重大損失或爭議 — BigDecimal 的精度至關重要。

3. BigDecimal 的基本用法

如何建立 BigDecimal 實例

與一般的數值常字串不同,BigDecimal 通常應 從字串建構。這是因為從 doublefloat 建立的值可能已經包含二進位近似錯誤。 推薦(從 String 建構):

BigDecimal value = new BigDecimal("0.1");

避免(從 double 建構):

BigDecimal value = new BigDecimal(0.1); // may contain error

如何進行運算

BigDecimal 無法使用一般的運算子 (+, -, *, /)。反而必須使用專用的方法。 加法 (add)

BigDecimal a = new BigDecimal("10.5");
BigDecimal b = new BigDecimal("2.3");
BigDecimal result = a.add(b); // 12.8

減法 (subtract)

BigDecimal result = a.subtract(b); // 8.2

乘法(multiply)

BigDecimal result = a.multiply(b); // 24.15

除法(divide)與捨入模式
除法需要特別小心。若除不盡,除非指定捨入模式,否則會拋出 ArithmeticException

BigDecimal a = new BigDecimal("10");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // 3.33

此處我們指定「保留 2 位小數」且「四捨五入」。

使用 setScale 設定尺度與捨入模式

setScale 可用於將數值捨入到指定的位數。

BigDecimal value = new Big BigDecimal("123.456789");
BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // 123.46

常見的 RoundingMode 值:

Mode NameDescription
HALF_UPRound half up (standard rounding)
HALF_DOWNRound half down
HALF_EVENBanker’s rounding
UPAlways round up
DOWNAlways round down

BigDecimal 為不可變物件

BigDecimal不可變 的。也就是說,算術方法(add、subtract 等)不會修改原始值——它們 會回傳一個新實例

BigDecimal original = new BigDecimal("5.0");
BigDecimal result = original.add(new BigDecimal("1.0"));
System.out.println(original); // still 5.0
System.out.println(result);   // 6.0

4. BigDecimal 進階用法

比較值:compareTo 與 equals 的差異

BigDecimal 中,有兩種比較值的方法:compareTo()equals()它們的行為不同

  • compareTo() 只比較數值(忽略 equals() 連尺度(小數位數)一起比較。
    BigDecimal a = new BigDecimal("10.0");
    BigDecimal b = new BigDecimal("10.00");
    
    System.out.println(a.compareTo(b)); // 0 (values are equal)
    System.out.println(a.equals(b));    // false (scale differs)
    

重點: 對於數值相等的檢查——例如金額相等——通常建議使用 compareTo()

從/轉換為字串

在使用者輸入與外部檔案匯入時,字串與 BigDecimal 之間的轉換很常見。
字串 → BigDecimal

BigDecimal value = new Big BigDecimal("1234.56");

BigDecimal → 字串

String str = value.toString(); // "1234.56"

使用 valueOf
Java 也提供 BigDecimal.valueOf(double val),但它同樣 內部帶有 double 的誤差,因此仍建議從字串建構較安全。

BigDecimal unsafe = BigDecimal.valueOf(0.1); // contains internal error

透過 MathContext 設定精度與捨入規則

MathContext 讓你一次設定精度與捨入模式——在大量運算中套用統一規則時非常有用。

MathContext mc = new MathContext(4, RoundingMode.HALF_UP);
BigDecimal result = new BigDecimal("123.4567").round(mc); // 123.5

也可用於算術運算:

BigDecimal a = new BigDecimal("10.456");
BigDecimal b = new BigDecimal("2.1");
BigDecimal result = a.multiply(b, mc); // 4-digit precision

null 檢查與安全初始化

表單可能傳入 null 或空值——加入防護程式碼是標準作法。

String input = ""; // empty
BigDecimal value = (input == null || input.isEmpty()) ? BigDecimal.ZERO : new BigDecimal(input);

檢查 BigDecimal 的尺度

若要得知小數位數,可使用 scale()

BigDecimal value = new BigDecimal("123.45");
System.out.println(value.scale()); // 3

5. 常見錯誤與解決方式

ArithmeticException:無限小數展開

錯誤範例:

BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
BigDecimal result = a.divide(b); // exception

這是「1 ÷ 3」——因為會產生無限小數,若未指定捨入模式或尺度,會拋出例外
解決方式:指定尺度 + 捨入模式

BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP); // OK (3.33)

直接以 double 建構時的錯誤

直接傳入 double 可能已帶有二進位誤差——會產生意外的值
錯誤範例:

BigDecimal val = new BigDecimal(0.1);
System.out.println(val); // 0.100000000000000005551115123...

正確:使用字串

BigDecimal val = new BigDecimal("0.1"); // exact 0.1

注意: BigDecimal.valueOf(0.1) 內部使用 Double.toString(),因此它「幾乎相同」於 new BigDecimal("0.1") — 但字串是最安全的 100% 方式。

由於 Scale 不匹配導致的 equals 誤解

因為 equals() 會比較 scale,即使數值相等也可能回傳 false。

BigDecimal a = new BigDecimal("10.0");
BigDecimal b = new BigDecimal("10.00");

System.out.println(a.equals(b)); // false

解決方案:用於數值相等的 compareTo()

System.out.println(a.compareTo(b)); // 0

由於精確度不足導致的意外結果

如果使用 setScale 而不指定捨入模式 — 可能會發生例外。 不良範例:

BigDecimal value = new BigDecimal("1.2567");
BigDecimal rounded = value.setScale(2); // exception

解決方案:

BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // OK

輸入值無效時的 NumberFormatException

如果傳入無法解析為數字的無效文字(例如使用者輸入 / CSV 欄位),會發生 NumberFormatException解決方案:使用例外處理

try {
    BigDecimal value = new BigDecimal(userInput);
} catch (NumberFormatException e) {
    // show error message or fallback logic
}

6. 實際使用範例

這裡介紹 真實世界情境,示範如何在實務中使用 BigDecimal。特別是在財務/會計/稅務計算中,精確數值處理的重要性顯而易見。

價格計算中的小數處理(捨入小數)

範例:計算包含 10% 消費稅的價格

BigDecimal price = new BigDecimal("980"); // price w/o tax
BigDecimal taxRate = new BigDecimal("0.10");
BigDecimal tax = price.multiply(taxRate).setScale(0, RoundingMode.HALF_UP);
BigDecimal total = price.add(tax);

System.out.println("Tax: " + tax);         // Tax: 98
System.out.println("Total: " + total);     // Total: 1078

重點:

  • 稅務計算結果通常處理為 整數 ,使用 setScale(0, RoundingMode.HALF_UP) 進行捨入。
  • double 容易產生錯誤 — 建議使用 BigDecimal

折扣計算(% OFF)

範例:20% 折扣

BigDecimal originalPrice = new BigDecimal("3500");
BigDecimal discountRate = new BigDecimal("0.20");
BigDecimal discount = originalPrice.multiply(discountRate).setScale(0, RoundingMode.HALF_UP);
BigDecimal discountedPrice = originalPrice.subtract(discount);

System.out.println("Discount: " + discount);         // Discount: 700
System.out.println("After discount: " + discountedPrice); // 2800

重點: 價格折扣計算 絕對不能損失精確度

單價 × 數量計算(典型商業應用情境)

範例:298.5 日圓 × 7 件

BigDecimal unitPrice = new BigDecimal("298.5");
BigDecimal quantity = new BigDecimal("7");
BigDecimal total = unitPrice.multiply(quantity).setScale(2, RoundingMode.HALF_UP);

System.out.println("Total: " + total); // 2089.50

重點:

  • 調整小數乘法的捨入。
  • 對會計 / 訂單系統很重要。

複利計算(財務範例)

範例:3% 年利率 × 5 年

BigDecimal principal = new BigDecimal("1000000"); // base: 1,000,000
BigDecimal rate = new BigDecimal("0.03");
int years = 5;

BigDecimal finalAmount = principal;
for (int i = 0; i < years; i++) {
    finalAmount = finalAmount.multiply(rate.add(BigDecimal.ONE)).setScale(2, RoundingMode.HALF_UP);
}

System.out.println("After 5 years: " + finalAmount); // approx 1,159,274.41

重點:

  • 重複計算會累積錯誤 — BigDecimal 可避免此問題。

使用者輸入的驗證與轉換

public static BigDecimal parseAmount(String input) {
    try {
        return new BigDecimal(input).setScale(2, RoundingMode.HALF_UP);
    } catch (NumberFormatException e) {
        return BigDecimal.ZERO; // treat invalid input as 0
    }
}

要點:

  • 安全地轉換使用者提供的數字字串。
  • 驗證 + 錯誤回退提升韌性。

7. 小結

BigDecimal 的角色

在 Java 的數值處理中——尤其是 金錢或需要高精度的邏輯——BigDecimal 類別是不可或缺的。使用 BigDecimal 可以大幅避免 float / double 內在的誤差。
本文涵蓋了基礎、算術、比較、四捨五入、錯誤處理以及實務範例。

重點回顧

  • BigDecimal 支援任意精度的十進位——非常適合金錢與高精度計算
  • 初始化應使用 字串常量,例如 new BigDecimal("0.1")
  • 使用 add()subtract()multiply()divide(),且在除法時必須指定四捨五入模式
  • 使用 compareTo() 來比較相等性——了解它與 equals() 的差異
  • setScale() / MathContext 讓你精細控制小數位與四捨五入
  • 真實的業務邏輯案例包括金錢、稅金、數量 × 單價 等

給即將使用 BigDecimal 的讀者

雖然「在 Java 中處理數字」看似簡單——精度 / 四捨五入 / 數值誤差問題 總是潛藏其後。BigDecimal 正是直接解決這些問題的工具——精通它即可撰寫 更可靠的程式碼
起初你可能會對四捨五入模式感到困惑——但隨著實際專案的使用,便會變得自然。
下一章將是 FAQ(常見問題),彙總關於 BigDecimal 的常見問題——有助於複習與特定語意搜尋。

8. FAQ:關於 BigDecimal 的常見問題

Q1. 為什麼要使用 BigDecimal 而不是 float 或 double?

A1.
因為 float/double 以二進位近似方式表示數字——十進位小數無法精確表示。這會導致像「0.1 + 0.2 ≠ 0.3」的結果。
BigDecimal 能精確保留十進位值——非常適合金錢或對精度要求極高的邏輯。

Q2. 建立 BigDecimal 實例的最安全方式是什麼?

A2.
永遠以字串建構
錯誤範例(錯誤):

new BigDecimal(0.1)

正確範例:

new BigDecimal("0.1")

BigDecimal.valueOf(0.1) 內部會使用 Double.toString(),結果相近——但以字串方式最安全。

Q3. 為什麼 divide() 會拋出例外?

A3.
因為當結果是無限循環數時,BigDecimal.divide() 會拋出 ArithmeticException
解決方案:指定小數位與四捨五入模式

BigDecimal result = a.divide(b, 2, RoundingMode.HALF_UP);

Q4. compareTo() 與 equals() 有何差異?

A4.

  • compareTo() 檢查 數值相等(忽略小數位)
  • equals() 檢查 完全相等,包括小數位
    new BigDecimal("10.0").compareTo(new BigDecimal("10.00")); // → 0
    new BigDecimal("10.0").equals(new BigDecimal("10.00"));    // → false
    

Q5. 如何執行四捨五入?

A5.
使用 setScale() 並明確指定四捨五入模式。

BigDecimal value = new BigDecimal("123.4567");
BigDecimal rounded = value.setScale(2, RoundingMode.HALF_UP); // 123.46

主要的四捨五入模式:

  • RoundingMode.HALF_UP(四捨五入,五捨六入)
  • RoundingMode.DOWN(向零方向捨去)
  • RoundingMode.UP(遠離零方向進位)

Q6. 我可以檢查小數位數(scale)嗎?

A6.
可以——使用 scale()

BigDecimal val = new BigDecimal("123.45");
System.out.println(val.scale()); // → 3

Q7. 如何安全地處理 null/空字串輸入?

A7.
必須始終加入 null 檢查 + 例外處理

public static BigDecimal parseSafe(String input) {
    if (input == null || input.trim().isEmpty()) return BigDecimal.ZERO;
    try {
        return new BigDecimal(input.trim());
    } catch (NumberFormatException e) {
        return BigDecimal.ZERO;
    }
}