整理 Effective Java 書中 Item 60: Avoid float and double if exact answers are required 心得筆記

主旨

Java 的 floatdouble 是為了高效處理近似值而設計,特別適合科學與工程領域。
但若場景需要精確數值(例如金額計算),它們就會成為麻煩來源,甚至導致錯誤結果。

點出問題:以為的 1.03 - 0.42,其實是 0.6100000000000001

浮點數並不能精確表達像 0.1 這樣的十進位小數。這會導致得到看起來荒謬的答案:

System.out.println(1.03 - 0.42); // 輸出:0.6100000000000001
System.out.println(1.00 - 9 * 0.10); // 輸出:0.09999999999999998

就算選擇四捨五入顯示,也無法保證運算結果在邏輯上正確。

範例:有 $1,要買 10¢、20¢、30¢…… 的糖果

以下這段用 double 計算你能買幾個糖果,並顯示剩下的錢:

double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
    funds -= price;
    itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Change: $" + funds);

輸出結果:

3 items bought.
Change: $0.3999999999999999

這根本就錯!你應該能買 4 個才對,並剛好把錢花光。

解法一:使用 BigDecimal

final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) {
    funds = funds.subtract(price);
    itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Money left over: $" + funds);

輸出正確:

4 items bought.
Money left over: $0.00

注意:建構 BigDecimal 請用字串建構子,如 new BigDecimal("1.00")避免使用 new BigDecimal(1.00),因為會帶入不精確的浮點值!

解法二:用 int 或 long 做「整數處理」

用整數代表最小單位(如:1 元 = 100 分),可以避開浮點誤差:

int itemsBought = 0;
int funds = 100;
for (int price = 10; funds >= price; price += 10) {
    funds -= price;
    itemsBought++;
}
System.out.println(itemsBought + " items bought.");
System.out.println("Cash left over: " + funds + " cents");

這種方法簡潔、快速、無誤差,只是需要自己處理單位轉換與格式化輸出。

比較三種做法

方法精確度執行速度撰寫便利性適用場景
float/double科學計算、不需完全精準的統計
BigDecimal金融、帳務、法規計算
int/long✅ (需自己轉換單位)小額金錢運算、遊戲內金幣等

小結

除非只需要「大約值」,否則千萬別用 floatdouble 來處理金額或其他需要絕對正確性的資料。

實務建議:

  • 金額精算 → 用 BigDecimal
    • 搭配字串建構子避免誤差
    • 支援進位規則(例如四捨五入)
  • 單位明確、金額不大 → 用 int/long
    • 1 元 = 100 分、1 天 = 86400 秒這種設計很常見
  • 若金額會超過 18 位數,才考慮用 BigDecimal 取代 long

精確比速度更重要的場景,請遠離 floatdouble