1. 介紹
Java 中數值計算的精確度問題
在 Java 程式設計中,數值計算每天都會進行。例如,計算產品價格、確定稅金或利息 — 這些操作在許多應用程式中都是必要的。然而,當使用浮點數類型如 float 或 double 進行此類計算時,可能會發生意外的錯誤。
這是因為 float 和 double 以 二進位近似值 表示數值。像「0.1」或「0.2」這樣在十進位中可以精確表示的值,在二進位中無法精確表示 — 因此,會累積小錯誤。
BigDecimal 對於貨幣或精確計算至關重要
此類錯誤在 貨幣計算和高精度科學/工程計算 等領域可能至關重要。例如,在計費計算中,即使是 1 日圓的差異也可能導致信譽問題。
這就是 Java 的 BigDecimal 類別的優勢所在。BigDecimal 可以 處理任意精度的十進位數字,並透過取代 float 或 double 使用它,可以無錯誤地進行數值計算。
閱讀本文您將獲得什麼
在本文中,我們將系統性地解釋 Java 中 BigDecimal 的基本用法、高級技巧,以及常見錯誤和注意事項。
這對於那些希望在 Java 中準確處理貨幣計算,或考慮在專案中採用 BigDecimal 的人很有用。
2. BigDecimal 是什麼?
BigDecimal 概覽
BigDecimal 是 Java 中的一個類別,能夠進行高精度十進位運算。它屬於 java.math 套件,並專門設計用於 不容許錯誤的計算,例如財務/會計/稅務計算。
使用 Java 的 float 和 double,數值以二進位近似值儲存 — 意味著像「0.1」或「0.2」這樣的十進位無法精確表示,這是錯誤的來源。相較之下,BigDecimal 以 基於字串的十進位表示 儲存數值,從而抑制捨入和近似錯誤。
處理任意精度數字
BigDecimal 最大的特徵是「任意精度」。整數和小數部分理論上可以處理 幾乎無限的位數,避免因位數限制而導致的捨入或位數遺失。
例如,下列大數字可以精確處理:
BigDecimal bigValue = new BigDecimal("12345678901234567890.12345678901234567890");
能夠像這樣在保持精度的同時進行運算,是 BigDecimal 的主要優勢。
主要使用案例
建議在以下情況中使用 BigDecimal:
- 貨幣計算 — 財務應用程式中的利息、稅率計算
- 發票 / 報價金額處理
- 需要高精度的科學/工程計算
- 長期累積導致錯誤累積的過程
例如,在 會計系統和薪資計算 中 — 1 日圓的差異可能導致重大損失或爭議 — BigDecimal 的精度至關重要。
3. BigDecimal 的基本用法
如何建立 BigDecimal 實例
與一般的數值常字串不同,BigDecimal 通常應 從字串建構。這是因為從 double 或 float 建立的值可能已經包含二進位近似錯誤。 推薦(從 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 Name | Description |
|---|---|
HALF_UP | Round half up (standard rounding) |
HALF_DOWN | Round half down |
HALF_EVEN | Banker’s rounding |
UP | Always round up |
DOWN | Always 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;
}
}