Java 浮動小数点数の誤差に立ち向かう


この記事のコードはJava SE8で書いています。

背景

double型の変数を==で比較していたら、静的解析ツールでそれダメだよ、バグだよ指摘を受ける。
色々気になったので、浮動小数点数の扱い方法を(今さら)ちゃんと調べてみた。

先に結論

特にパフォーマンス気にしないのであれば、BigDecimal使おうね。
データベースとかの設計変えれるなら、桁ずらしてintやlongで扱ってもおk。

問題

まずはこいつを見てほしい。

コード

    double doubleValue1 = 1.0d;
    double doubleValue2 = 0.9d;
    double resultValue = doubleValue1 - doubleValue2;

    System.out.println("doubleValue1: " + doubleValue1);
    System.out.println("doubleValue2: " +  doubleValue2);
    System.out.println(resultValue == 0.1d);
    System.out.println("減算結果: " + resultValue);

実行結果

    doubleValue1: 1.0
    doubleValue2: 0.9
    false
    減算結果: 0.09999999999999998

減算結果がえらいことになっている。

コンピュータは数字を2進数で扱っている。

しかし小数には0.9のように、2進数で表すと循環小数になっちゃうものがある。
コンピュータでは有限桁数の数しか扱えないので、適当な値に丸められ誤差が生じる。

微妙に0.9じゃない値で減算しているため、上記のような結果になっちゃう。
勘定系システムにおいては1円の誤差でも訴訟にまで発展しかねないので、大問題である。

解決策

BigDecimalを使えば解決!

コード

    BigDecimal bigDecimalValue1 = new BigDecimal("1.0");
    BigDecimal bigDecimalValue2 = new BigDecimal("0.9");
    BigDecimal bigDecimalResultValue = bigDecimalValue1.subtract(bigDecimalValue2);

    System.out.println("bigDecimalValue1: " + bigDecimalValue1);
    System.out.println("bigDecimalValue2: " + bigDecimalValue2);
    System.out.println(bigDecimalResultValue.equals(new BigDecimal("0.1")));
    System.out.println("減算結果: " + bigDecimalResultValue);

実行結果

    bigDecimalValue1: 1.0
    bigDecimalValue2: 0.9
    true
    減算結果: 0.1

BigDecimalでは小数部分を整数部分にシフトした形式で値を保持する。
全ての整数は有限な2進数で表現できるので、BigDecimalを使えば正確に浮動小数点数の演算、比較が可能。

メソッドは公式リファレンス参照

速度

BigDecimalは参照型なので、基本データ型の演算に比べると当然遅い。
確認してみよう。

コード

    double doubleValue1 = 1.0d;
    double doubleValue2 = 0.9d;

    long startDouble = System.currentTimeMillis();

    // 1億回減算実施(double)
    for(int i = 0; i < 100_000_000 ; i++){
        double resultValue = doubleValue1 - doubleValue2;
    }

    long endDouble = System.currentTimeMillis();

    BigDecimal bigDecimalValue1 = new BigDecimal("1.0");
    BigDecimal bigDecimalValue2 = new BigDecimal("0.9");

    long startBigDecimal = System.currentTimeMillis();

    // 1億回減算実施(BigDecimal)
    for(int i = 0; i < 100_000_000 ; i++){
        BigDecimal bigDecimalResultValue = bigDecimalValue1.subtract(bigDecimalValue2);
    }

    long endBigDecimal = System.currentTimeMillis();

    System.out.println("double 計測結果: " + (endDouble - startDouble) + "ms");
    System.out.println("bigDecimal 計測結果: " + (endBigDecimal - startBigDecimal) + "ms");

実行結果

    double 計測結果: 3ms
    bigDecimal 計測結果: 314ms

結構差が出た。てか基本データ型ってやっぱり速いのね!

まあループがめちゃくちゃ多かったり、よっぽどパフォーマンスを要求される場合じゃなければBigDecimalで対応しても問題ないのかなーと個人的には思ってみたり。

逃げれたら逃げる

別に無理やり浮動小数点数を使わなくてもいい場合ってありますよね。

例えば体重を小数第1桁まで登録するシステムがあったとします。
インタフェース的には65.3kgとかで受け取っても、プログラムの上流部分で1個シフトさせて内部的に653とかで扱えば、普通にintで処理できます。煩わしい浮動小数点数処理から解放される!

ただデータベースのカラム定義とかにも影響するので、設計段階でしっかり決めるのが吉ですね~。