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 ユーロの取引を合計するとします。各反復で 0.00001 ユーロずつ「失う」プログラムだったら、銀行全体の規模では実際にお金を「失う」ことになります。すると経理がやって来てこう言うでしょう: 「お金はどこへ消えたのですか?!」

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("ほぼ等しい!"); // この方法の方が安全
}

ここでは「2 つの数の差が 100 万分の 1 より小さければ、等しいと見なす」と言っています。
注: 関数 Math.abs(value) は、渡された数値の絶対値を返します。

4. 特別な値 double: InfinityNaN-Infinity

double 型は通常の数だけでなく、特別な値も保持します。これは、数学では学生に厳しく禁じるような状況で発生します。

無限大(Infinity

10 で割るとどうなるでしょう?

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));         // NaN なら true

表: 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 を返します。これは静かで見つけにくいバグを招くことがあります — 結果は出るものの、期待どおりではありません。

コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION