背景
先日、同僚からdoubleをBigDecimalに変換するとき適当にやると誤差がでますよね
と言われ ???? となってしまいました😇
恐れながら、BigDecimal ついてちゃんと理解しておらず、何も考えずにdoubleをBigDecimalに変換し、数値の誤差をだして、計算がずれるというのを今後やらかしそうだったので、
なぜ、doubleをBigDecimalに変換すると誤差が発生するのかについて調べたり確認したことをまとめておこうと思います😅
どのような状況で誤差が発生するのか
小数が含まれる時に発生する可能性があります。
double tmp = 0.5;
System.out.println( new BigDecimal(tmp) );
// 出力結果 0.5
double tmp = 0.3;
System.out.println( new BigDecimal(tmp) );
// 出力結果 0.299999999999999988897769753748434595763683319091796875
「0.5」、「0.3」と同じような値でもこのように誤差がでたりでなかったりします。
なぜ、誤差が発生するのか
原因としては、
Double 公式リファレンス
で記載があるように、Double型は、IEEE754 に準拠しているためです。
どういうことかと言いますと、プログラムとして、doubleを記述する際には10進数で表現していますが、ソフトウェア内部での数値は2進数で扱われます。
つまり、10進数で表記したものが、内部では2進数に変換し保持され、コンソールなどに出力される際に、2進数から10進数に変換されるということになります。
10進数 -> 2進数 -> 10進数
といった流れとはなり、誤差が発生します。
具体的に先ほどの誤差が発生した時と発生しなかった時の例で見てみると
誤差が発生しない例
10進数「0.5」 を 2進数に変換すると、「0.1」となります。
2進数「0.1」を 10進数に変換すると、「0.5」となり、完全に元の値に復元が行えました。
誤差が発生する例
10進数「0.3」 を 2進数に変換すると、「0.010011001100...」となり、
2進数「0.010011001100」を 10進数に変換すると、「0.2998046875」となります。
0x0.1は、有限小数に対して、0b0.010011001100は、循環節が「1100」の循環小数となります。
double型の桁数は有限であるため、無限小数は必ず丸め込みが発生し、誤差が出てします。
では、誤差がでないようにするにはどうするのか
BigDecimal を使用しましょう!!!
BigDecimalは、内部的に
unscaledValue×10^{-scale}
というように小数を整数として保持するため、誤差が発生しないようにしています。
整数は、小数と異なり、どんな10進数の値でも、綺麗に2進数で表現することができます。
10進数 -> 2進数 -> 10進数
と変換を続けても誤差は発生しません。
例とし、10進数「0.3」 を保持する場合について考えてみる
0.3 は、
3 × 10^{-1}
と表現でき、整数部「3」は、
2進数「0011」となります。
2進数「00111」を10進数に変換しても「3」と綺麗に元の値に復元が行えます。
このように、小数を整数として扱うようにすることで、BigDecimalは、小数でも誤差がでないようにしています。
我々は普段10進数を使用し物事を考え表現していますが、ソフトウェアは2進数で動くということを改めて考えさせらますね。。。
BigDecimalを使用する際の注意
小数を引数としたBigDecimalのコンストラクタを利用する際には、注意が必要となります。
上で書いた例のように
double tmp = 0.3;
System.out.println( new BigDecimal(tmp) );
// 出力結果 0.299999999999999988897769753748434595763683319091796875
になることがあるため、小数をBidDecimalに変換する場合は、引数を「数値型」ではなく、「文字列」で初期化をするようにします。
System.out.println( new BigDecimal( "0.3" ) );
// 出力結果 0.3
のように、誤差が発生しなくなります。
BigDecimal.valueOf() などは、引数が数値型でも内部で、文字列に変換しキャストすることで、誤差が発生しないようにしているようです。
BigDecimalを使用しても誤差が出る場合はありますので、用法用量を守り正しくお使いください。
おわりに
金額計算を行うなら、BigDecimalを使用すると、Javaを勉強し始めで教えられるかと思います。
ただ、なぜ、BigDecimalを使用しなければいけないのかという理由まで理解していなければ、冒頭で書いたように、
先日、同僚からdoubleをBigDecimalに変換するとき適当にやると誤差がでますよね
と言われ ???? となってしまいました😇
となってしまします。
無知ってのは怖いですね(笑)
言語の仕様について、普段はブラックボックスとして見ない部分も、本当に理解をするためにはやはり、奥深くまで潜らなければ理解することはできないってことを改めて実感させられました💦