計算機叫「計算」機,卻連最基本的小數加法都算不準?`0.1 + 0.2 = 0.30000000000000004`。這不是 Bug,而是**二進制浮點數表示的必然結果**。本文深入原理,給出工程解決方案。
System.out.println(0.1 + 0.2); // 0.30000000000000004
System.out.println(0.1 * 0.1); // 0.010000000000000002
System.out.println(1.0 - 0.9); // 0.09999999999999998
十進制中 `1/3 = 0.333...` 無限循環,截斷存儲必然誤差。同理,二進制中許多十進制有限小數變成**無限循環小數**。
二進制小數位權:`2⁻¹=0.5, 2⁻²=0.25, 2⁻³=0.125, 2⁻⁴=0.0625...`
只有能表示為這些位權**有限和**的數才能精確表示。
計算機只能截斷/捨入存儲有限位 (float 23位尾數, double 52位),**存入的已非真實 0.1**,運算誤差必然累積。
底層硬件 (電晶體) 只有高/低電壓兩種穩定狀態 → 0/1。二進制電路最簡單、可靠、高效。十進制需 10 種狀態區分,電路複雜度指數級上升,抗干擾性下降。
定義兩種主流格式:
| 格式 | Java 類型 | 總位數 | 符號位 | 指數位 | 尾數位 | 範圍大致 |
|---|---|---|---|---|---|---|
| 單精度 | float | 32 | 1 | 8 | 23 | ±3.4E38 |
| 雙精度 | double | 64 | 1 | 11 | 52 | ±1.7E308 |
科學計數法形式:`(-1)^符號 × 1.尾數 × 2^(指數-偏移量)`
System.out.println(0.1 + 0.1); // 0.2 (看起來準)
結果與 `0.2` 極其接近,Java `toString()` / `println` 內部有**智能格式化邏輯**:若誤差極小,輸出最短的能唯一反映該二進制值的十進制字符串 (如 `0.2` 而非 `0.20000000000000001`)。**實際內存值依然不精確**,只是顯示欺騙了你。
// 保留 2 位小數
System.out.printf("%.2f", 0.1 + 0.2); // 0.30
// 或
BigDecimal bd = new BigDecimal(0.1 + 0.2).setScale(2, RoundingMode.HALF_UP);
將小數放大為整數 (如分為單位),計算完成再縮小。
long price1 = 199; // 1.99 元 -> 199 分
long price2 = 299; // 2.99 元
long sumCent = price1 + price2; // 498 分
System.out.println(sumCent / 100.0); // 4.98
// 必須用字符串構造器,避免 double 構造器帶入二進制誤差
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");
BigDecimal sum = a.add(b); // 0.3 精確
System.out.println(sum); // 0.3
// 除法需指定精度與舍入模式
BigDecimal div = a.divide(b, 10, RoundingMode.HALF_UP);
性能代價:`BigDecimal` 為不可變對象,運算創建新對象,比基本類型慢 1-2 個數量級。僅在業務要求精確處使用,高頻計算循環避免使用。
// float
System.out.println(Integer.toBinaryString(Float.floatToIntBits(0.1f)));
// double
System.out.println(Long.toBinaryString(Double.doubleToLongBits(0.1)));