浮動小数点数
数値に関する正確な答えが知りたい場合、float と double の利用は避けるべきです。
float と double は2進数浮動小数点数で表されます。 浮動小数点数は広い範囲の数値計算に対して、近似をすばやく行うために設計された小数計算方式です。 そのため、金銭計算などの精確な計算要求される分野では float と double は適していません。
たとえば、1 ドルで 10 セント、20セント、30セントと値上がりする商品をいくつ購入できるかを調べるプログラムを作成します。 理論上は 0.1 + 0.2 + 0.3 + 0.4 = 1.0 で 4 個の商品を買えるはずです。
double funds = 1.00; int itemsBought = 0; for (double price = .10; funds >= price; price += .10) { funds -= price; itemsBought++; } System.out.println(itemsBought + " items bought."); // => 3 items bought. System.out.println("Change: $" + funds); // => Change: $0.3999999999999999
しかし、実行してみると分かるように、double を使うと計算に誤差が発生するため、3 個しか購入できません。
BigDecimal
正確な数値計算を行うためには BigDecimal を用いるべきです。 BigDecimal は任意精度の符号付き小数を扱うことができるので、正確な値を保持することができます。
先ほどの金銭計算を BigDecimal で書き換えると以下のようになります。
final BigDecimal TEN_CENTS = new BigDecimal(".10"); int itemsBought = 0; BigDecimal funds = new BigDecimal("1.00"); for (BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) { itemsBought++; funds = funds.subtract(price); } System.out.println(itemsBought + " items bought."); // => 4 items bought. System.out.println("Change: $" + funds); // => Change: $0.00
しかし、BigDecimal には次のような欠点があるので注意が必要です。
- 計算が遅い
- 算術が不便
固定小数点
もし、BigDecimal の遅さが許容できない場合には int や long を代用する方法があります。 この場合、どの桁に小数点があったかをプログラマが随時把握しておく必要があります。 この方式は固定小数点方式と呼ばれることもあります。
以下のプログラムは固定小数点方式で実装されています。 すべて cent で表されているため、int を用いることができるのです。
int itemsBought = 0; int funds = 100; for (int price = 10; funds >= price; price += 10) { itemsBought++; funds -= price; } System.out.println(itemsBought + " items bought."); // => 4 items bought. System.out.println("Change: $" + funds); // => Change: $0
ただし、int の場合は 9 桁、long の場合は 18 桁の精度でしか利用できません。
感想
固定小数点は誤差がないように見えて、実際にはそうではない。 結局、固定小数点で表せる範囲は有限なわけで、その範囲を超えると誤差が出る。
特に掛算をするとあっという間にオーバーフローして誤差が出る。 例えば、0.858 * 1.302 は 1.117116 になるので、もし正確な値を保持したければ8桁の精度が必要になってしまう。
もろもろ考えると浮動小数点はよく出来ているなぁと思う。