なぜBigDecimalを使わなければならないのか

「floatやdoubleでも別に計算できるよ」、「なんでめんどくさいBigDecimalなんて使うのか?」といった疑問にお答えします。

double/floatでは意図した計算ができないことが問題

double/floatでもちゃんと計算はしているが意図した結果にならない(できない)。意図しない計算は意図しない誤差を生んでバグと判断される。これが問題。まずは、なぜ意図した計算ができないのかを探る。

計算に丸め方を指定できない

これは次の例から理解できる。

double result = 10d / 6d;
System.out.println(result);

出力は次のようになる。

1.6666666666666667

本来は1.6666666…のように無限に続くはずであるが、丸められてしまう。

この計算結果は無限小数なのでいずれは丸めなければ数値表現はできない。よって意図した計算をするには、じつは丸め方法を指定することが必要である。ただしdobuleやfloat等のプリミティブ型の計算ではそれを指定できない。

これが意図した計算ができない1つ目の理由。丸めをコントロール(指定)できないので、コントロールできない誤差が生まれてしまう。

そもそも表現できない10進小数がある

これも例を出すと次のような数の場合

double result = 1.0000000000000000005d;
System.out.println(result);

出力は次のようになる。

1.0

メモリに乗せただけで丸められてしまう。これは浮動小数点方式であることに起因する。浮動小数点方式で困る点は、例えば次の2つ。

  • 2進数では循環する10進小数は、丸められてしまう。

    • (10進)小数には2進数にすると循環してしまう数値がある。例えば「0.1」は、2進数にすると「0.001100110011…」。無限に続いてしまうので、どっかで丸めることになる。

  • 桁数に制限がある。

    • 浮動小数点方式は固定長なので、限界が来ると丸めが発生する。

浮動小数点は工夫された2進数の組合せみたいなモノ。結局2進数の問題は引き継いでしまうし、桁制限もされている。さらになぜ丸められてしまったのかの理由も特定できない。

これが意図した計算ができない2つ目の理由。計算する以前に意図しない丸めが発生してしまえば、意図した計算はできない。

意図した計算をする仕組みが整えられたBigDecimal

double/floatでは意図した計算をするのが無謀なので、Javaではそれに対応するためBigDecimalが用意されている。

意図しない丸めを許さない

double/floatのプリミティブの計算では丸めが指定できなかったが、BigDecimalでは計算が全てメソッドなので引数として丸め方法を渡すことができる。

例 小数点2位で四捨五入する

BigDecimal six = BigDecimal.valueOf(6);
BigDecimal ten = BigDecimal.TEN;
//RoundingMode.HALF_UPは四捨五入
BigDecimal result = ten.divide(six, 2, RoundingMode.HALF_UP);
System.out.println(result);

出力

1.67

丸めモード無指定でも計算できるが、計算結果が無限小数になるとArithmeticExceptionが投げられるので、計算結果が得られない。

BigDecimal six = BigDecimal.valueOf(6);
BigDecimal ten = BigDecimal.TEN;
// 計算結果が無限小数になり丸めモードの指定が無いため、ArithmeticException
BigDecimal result = ten.divide(six);

無限小数になると例外になるので、その可能性がわずかでもあれば(特に除算)丸めを指定しなければならなくなる。このようにBigDecimalは仕組みとして、意図しない丸めしないし許さない。

整数と10のマイナス乗で表す

浮動小数点方式により丸めてしまう問題については、整数と10のマイナス乗による表現で回避されている。BigDecimalは内部的には次のように値を保持している。

unscaledValue×10 -scale

整数であれば小数と違い2進数で循環することはないので、それによる丸めは心配ない。10のマイナス乗で表現することで値としても小数と同値を表現可能になっている。これによって、メモリに乗せただけで丸められてしまうこともなくなる。

桁数についても200億桁ぐらいまでいけるようなので、ほんとうに特殊な分野以外の人は考慮する必要も無い。

かといって、全てBigDecimalということにもならない

ここまでの話からすると、全部BigDecimalにすればいいと感じるがそうはならない。

この程度の誤差は本質的に無視できるモノが存在する。例えば、ゲームにおける座標計算。ゲームのキャラクターがジャンプした先の座標を計算するのに小数何十桁の精度はいらない。人間が感知できないからである。私たちはコンピュータに対して絶対の正確性を要求したがるが、ある程度の誤差であれば全然問題ない場面も多い。double/floatの誤差はその範囲には収まるレベル。

これにBigDecimalはいちいち丸めを指定することでコードが複雑になったり、計算が遅かったりということが加わるので、BigDecimal1択とは限らない。

一方お金の計算をするいわゆる勘定系は、1円単位の間違えでも信用を失う。ここではとにかく正確さなのでコードが複雑になろうがBigDecimal1択となる。こういったところではBigDecimalをしっかり使えないといけない。コンパイルが通り計算式も正しいが1円単位で誤差がでるなんてことになり、とてもやっかい。テストで見つけられるとも限らない。

結局は仕様によって判断する。悪なのはdouble/floatにおけるプリミティブ型計算の特徴を知らずに、なんとなく計算してしまうこと。知っているか知らないかの問題なので、今まで知らなかったらこれから直していけばいい。