Code Fix

中級

Javaで0.1 + 0.2 が 0.3 にならない理由

2進小数の限界です。誤差を許容する比較と、金額計算でBigDecimalを使う理由を扱います。

なぜエラーが出ないのか

出力: false
(エラーなし)
javacは何も報告しません。文法として正しいためです
出力: false
実際の挙動 — 期待した結果と食い違っている箇所
見つけ方
エラーが出ないので、出力を目で確かめるしかありません。この種の誤りが最も発見が遅れます

このエラーが出る典型パターン

パターン1

 1  public class Main {
 2      public static void main(String[] args) {
 3          double a = 0.1 + 0.2;
 4          System.out.println(a == 0.3                );
                                           ^
 5      }
 6  }
出力: false期待: true (コンパイルは通っています)

2進小数では0.1が正確に表せません。浮動小数点の比較は誤差を許容する形で書きます。

直し方: a == 0.3 を Math.abs(a - 0.3) < 1e-9 にします。

この問題を解いてみる →

広告
広告スロット(未設定)

パターン2

 1  public class Main {
 2      public static void main(String[] args) {
 3          System.out.println(0.1 + 0.2 == 0.3);
                                          ^
 4      }
 5  }
出力: 5.551115123125783E-17(誤差そのものが見えます)

差を取ると誤差の実体が見えます。金額計算にはBigDecimalを使うのが正解です。

直し方: == を - にします。

この問題を解いてみる →

パターン3

 1  import java.math.BigDecimal;
 2  
 3  public class Main {
 4      public static void main(String[] args) {
 5          BigDecimal a = new BigDecimal(0.1         );
                                                ^
 6          System.out.println(a);
 7      }
 8  }
出力: 0.1000000000000000055511151231257827021181583404541015625期待: 0.1 (コンパイルは通っています)

doubleを渡すと誤差ごと取り込まれます。BigDecimalには文字列で渡すのが鉄則です。

直し方: 0.1 を "0.1" にします。

この問題を解いてみる →

パターン4

 1  public class Main {
 2      public static void main(String[] args) {
 3          double sum = 0;
 4          for (int i = 0; i < 10; i++) sum += 0.1;
 5          System.out.println(sum == 1.0                );
                                            ^
 6      }
 7  }
出力: false期待: true (コンパイルは通っています)

0.1を10回足しても2進小数の誤差で正確に1.0にはなりません。誤差を許容する比較にします。

直し方: sum == 1.0 を Math.abs(sum - 1.0) < 1e-9 にします。

この問題を解いてみる →

パターン5

 1  import java.math.BigDecimal;
 2  
 3  public class Main {
 4      public static void main(String[] args) {
 5          BigDecimal a = new BigDecimal("1.0");
 6          BigDecimal b = new BigDecimal("1.00");
 7          System.out.println(a.equals(b)                 );
                                             ^
 8      }
 9  }
出力: false期待: true (コンパイルは通っています)

BigDecimalのequalsはスケール(桁数)も比較するため、1.0と1.00は等しくありません。数値として比較するにはcompareToを使います。

直し方: a.equals(b) を a.compareTo(b) == 0 にします。

この問題を解いてみる →

よくある誤解

floatをdoubleに変えても解決しません。精度が上がるだけで、2進小数で表せない値がある事実は変わりません。

実務での勘所

BigDecimalを使えば安全というわけでもありません。new BigDecimal(0.1)のようにdouble値をそのままコンストラクタに渡すと、すでに2進小数として丸められた後の不正確な値を引き継いでしまい、0.1000000000000000055511151231257827021181583404541015625のような値になります。正しく使うにはBigDecimal.valueOf(0.1)か、new BigDecimal("0.1")のように文字列から生成する必要があります。「BigDecimalを使った」だけでは不十分で、どう生成したかが重要です。

演習をはじめる

関連するエラー

広告
広告スロット(未設定)