CodeGym /課程 /JAVA 25 SELF /精確度問題與特殊值

精確度問題與特殊值

JAVA 25 SELF
等級 6 , 課堂 3
開放

1. 前言

看起來電腦「很聰明」,那 0.1 + 0.2 應該就會是 0.3。但事實並非如此。我們用一個簡單的例子來說明。


double x = 0.1;
double y = 0.2;
double sum = x + y;
System.out.println(sum); // 程式會輸出什麼?
浮點數相加:預期 0.3,實際上呢?

現在試著與 0.3 比較:

System.out.println(sum == 0.3); // 這裡會是 true 還是 false?

如果你看到 false,別驚訝!

原因在於數值在記憶體中的表現方式

電腦以二進位來處理數字。但並非所有十進位小數都能用有限的二進位小數表示,就像 1/3 無法用有限的十進位小數精確寫成(0.333...)。例如,0.1 的二進位表示是無窮小數,儲存時只能「四捨五入」成近似值。

打個比方,double 有時候「看起來」好像精確地存了你的數字,但實際上只是存了一個非常接近的近似值。

2. 使用 double 進行運算時會出現哪些「怪現象」?

來看一些實際範例。

範例 1:經典的 0.1 + 0.2「魔術」

double a = 0.1;
double b = 0.2;
double sum = a + b;
System.out.println(sum);            // 0.30000000000000004
System.out.println(sum == 0.3);     // false

電腦輸出的不是 0.3,而是 0.30000000000000004。差異很小,但若你從事的是例如金融等領域——這就很關鍵了。

範例 2:反覆累加

double result = 0;
for (int i = 0; i < 10; i++)
{
    result += 0.1;
}
System.out.println(result); // 0.9999999999999999

本來想得到 1.0——結果卻略小一些。原因依舊是 double 內部的捨入造成的。

為什麼在實務中很重要

很多人會想:「差那一點點,有什麼關係?」我們來看一個支付領域的例子。

假設你的網路銀行要把 100 筆、每筆 0.1 歐元的交易加總。如果你的程式每次迭代「流失」十萬分之一歐元,放到銀行的量級上就會「真的」少掉錢。這時財務會立刻找上你問:「我們的錢去哪了?!」

3. 如何正確比較浮點數

由於 double 往往無法精確儲存你期望的值,直接用 == 比較可能會出問題。通常的做法是比較兩數差的絕對值是否小於某個非常小的數(epsilon)。

帶容許誤差的比較範例

double a = 0.1 + 0.2;
double b = 0.3;
double epsilon = 0.000001;

if (Math.abs(a - b) < epsilon)
{
    System.out.println("幾乎相等!"); // 這樣比較更安全
}

這裡的意思是:「若兩個數的差小於一百萬分之一,就視為相等」。
說明:函式 Math.abs(value) 會回傳傳入數值的絕對值(模)。

4. double 的特殊值:InfinityNaN-Infinity

double 不只儲存一般數字,還有一些特殊值。它們出現在數學老師通常會嚴格禁止的情況。

無窮大(Infinity

若用 1 除以 0 會怎樣?

double result = 1.0 / 0.0;
System.out.println(result); // Infinity

在 Java(以及許多語言)中,對 double 進行被 0 除不會丟出例外!相反地,結果會成為特殊值「正無窮大」。

負無窮大(-Infinity

若用負數除以 0,就會得到負無窮大:

double result = -1.0 / 0.0;
System.out.println(result); // -Infinity

「非數值」(NaN — Not a Number)

如果做了一件很「奇怪」的事,例如試著對負數開平方根:

double result = Math.sqrt(-1);
System.out.println(result); // NaN

或是做 0.0 / 0.0

double result = 0.0 / 0.0;
System.out.println(result); // NaN

NaN 表示「在現實世界裡不會被視為數字的一切結果」。

如何檢查特殊值

Java 提供了用來檢查特殊值的函式:

System.out.println(Double.isInfinite(result));    // true,若為無窮大或無窮小
System.out.println(Double.isNaN(result));         // true,若為 NaN

表格:double 對非常規運算的反應

運算 結果 double 中實際儲存
1.0 / 0.0
Infinity +∞
-1.0 / 0.0
-Infinity -∞
0.0 / 0.0
NaN 非數值
Math.sqrt(-1)
NaN 非數值

5. 處理浮點數時的常見錯誤

錯誤 №1: 以 == 比較小數
最常見的陷阱:試圖用一般相等比較來判斷兩個浮點運算結果是否相等。由於捨入誤差會累積,你幾乎一定會得到出乎意料的結果。務必使用帶容許誤差(epsilon)的比較。

錯誤 №2: 計算中出現意料之外的 NaNInfinity
若不控制被 0 除或對負數開平方根,程式中可能出現 NaNInfinity,並「污染」之後的所有計算。別忘了使用 Double.isNaN()Double.isInfinite() 檢查可疑的值,避免不愉快的驚喜。

錯誤 №3: 用 NaN 當作「標記」
有些新手會把 NaN 當成「特殊」回傳值,例如「找不到就回傳 NaN」。但請記住:NaN 很棘手,用 == 比較不會奏效!只能透過專用方法來檢查。

錯誤 №4: 期待對 0.0 進行除法會丟例外
與整數除法不同,對 0.0 進行除法在 double 中不會拋出錯誤,而是回傳 InfinityNaN。這可能導致靜默且難以察覺的 bug——有結果,但不是你預期的。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION