正確な数値計算

BigDecimalについて今回は紹介します。
プログラミングにおける計算誤差とは?
Javaのfloat/doubleのような浮動小数点型は、扱い方によっては計算誤差が発生します。
金額計算のように「1円のズレが困る」場面では、誤差が致命的になることもあります。
そこで本記事では、なぜ誤差が出るのかと、Javaで誤差を避けて計算する方法(BigDecimal)をまとめます。
計算誤差が発生するケース

早速どんなことが起きるか紹介するよ!
まずは、誤差が出る有名な例を見てみましょう。
System.out.println(0.1 + 0.2); // 0.30000000000000004本来なら0.3になってほしいのですが、実際には0.30000000000000004のような値になることがあります。
なぜ誤差が生まれるか?
一見すると「バグ」に見えますが、これは浮動小数点型の性質です。

Javaに限らず多くのプログラミング言語で同じ現象が起こります!
誤差が発生するメカニズム

なんでなんだろう?
普段わたしたちが扱う数は10進数ですが、コンピューター内部では0と1で表現するため2進数で計算します。
2進数における少数
ここまでが概念的な話だったので、もう少し具体的に説明します。
- 2進数の整数は、桁が繰り上がるごとに2倍になります(10進数が10倍になるのと同じ)
- 2進数の小数は、桁が右に行くごとに1/2、1/4、1/8…となります(10進数の1/10、1/100…に相当)

切りが悪いのは知らなかったなー
問題は、0.1や0.2のような10進小数が、2進数では無限に続く循環小数になりやすいことです!
コンピューターは有限のビット数しか使えないため、どこかで打ち切って近似値として保持します。
その結果、0.1 + 0.2のような計算でもわずかなズレが生まれます。
一方で、0.5(= 1/2)や0.25(= 1/4)のように2進数で有限桁で表せる数は、比較的きれいに表現できます。
正確な計算をするために
では「プログラミングでは小数計算は誤差ありき」なのでしょうか?
もちろんケースバイケースですが、金額のように10進で正確に扱いたい小数を扱う場合は、浮動小数点型ではなく、10進小数として扱える仕組みを使うのが定石です。
BigDecimalについて
BigDecimalは10進数の小数を「値」と「小数点位置(スケール)」として保持し、10進数として計算します。

・値は3
・小数点を1つ左にずらす
3を10分の1することになるため0.3となる
このため内部的には整数計算に近い扱いになり、必要な桁数で正確に計算できます。
BigDecimalの使用方法
BigDecimalは次のようにインスタンスを生成します(生成方法は後述の「注意点」も必読です)。
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.2");intやdoubleのように算術演算子(+や*)では計算できないため、BigDecimalに用意されているメソッド(add/multiplyなど)で計算します。
BigDecimalの主要メソッド

BigDecimalの代表的なメソッドは以下の通りです!
| メソッド | 概要 | 例 |
|---|---|---|
| add(BigDecimal) | 加算 | a.add(b) |
| subtract(BigDecimal) | 減算 | a.subtract(b) |
| multiply(BigDecimal) | 乗算 | a.multiply(b) |
| divide(BigDecimal) / divide(BigDecimal, …) | 除算(割り切れない場合は丸め指定が必要) | a.divide(b, 2, RoundingMode.HALF_UP) |
| setScale(int) / setScale(int, RoundingMode) | 小数点以下の桁数(スケール)の調整 | a.setScale(2, RoundingMode.HALF_UP) |
| compareTo(BigDecimal) | 大小比較(equals と違い、スケール差を無視して比較) | a.compareTo(b) \> 0 |
サンプルコード
0.1¥+¥0.2¥がどうなるか、doubleとBigDecimalで見比べてみましょう。
// double
System.out.println(0.1 + 0.2); // 0.30000000000000004
// BigDecimal
BigDecimal x = new BigDecimal("0.1");
BigDecimal y = new BigDecimal("0.2");
System.out.println(x.add(y)); // 0.3doubleは0.30000000000000004のように誤差が出ることがありますが、BigDecimalでは0.3が出力されます。
BigDecimalを使う上での注意点

ここが一番ハマりどころなので、必ず押さえておこう!
1. new¥BigDecimal(double)は避ける
BigDecimalを使っても、作り方を間違えると最初から誤差を持った値を保持してしまいます。
// 非推奨
BigDecimal a = new BigDecimal(0.1);
System.out.println(a);これは0.1がdoubleとして表現された瞬間に誤差を含むためです。
基本は次のどちらかにしましょう!
- 文字列から作る(最も分かりやすい)
- new¥BigDecimal("0.1")
- BigDecimal.valueOf(double)を使う(内部でDouble.toStringを利用)
- BigDecimal.valueOf(0.1)
2. equalsではなくcompareToを使って比較する
BigDecimalのequalsは値だけでなくスケール(小数点以下の桁数)も比較します。
BigDecimal a = new BigDecimal("0.0");
BigDecimal b = new BigDecimal("0.00");
System.out.println(a.equals(b)); // false
System.out.println(a.compareTo(b)==0); // true「金額が同じか?」のような意味で比較したい場合はcompareToが安全です。
3. divide¥は丸めを指定しないと例外になることがある
BigDecimalの除算は、割り切れない結果になるとArithmeticExceptionが発生します。
BigDecimal a = new BigDecimal("1");
BigDecimal b = new BigDecimal("3");
// a.divide(b); // これは例外になり得る
BigDecimal c = a.divide(b, 2, RoundingMode.HALF_UP); // OK割り算が出てくる処理では、スケール(桁数)と丸め方式をセットで考える癖をつけると安全です。
金額計算の例(よくあるケース)
例えば税込計算のように「小数が出るかもしれない金額」を扱う場面を考えます。
BigDecimal price = new BigDecimal("1980");
BigDecimal taxRate = new BigDecimal("0.10");
BigDecimal tax = price.multiply(taxRate);
BigDecimal total = price.add(tax);
// 円の場合、小数点以下は基本的に不要なので丸める
total = total.setScale(0, RoundingMode.HALF_UP);
System.out.println(total);「最終的に整数(円)にする」「端数処理をどうするか(四捨五入/切り捨て/切り上げ)」など、業務ルールに合わせてsetScaleとRoundingModeを選ぶのがポイントです。
まとめ
BigDecimal を使った正確な数値計算の紹介でした!
今回の記事を簡単にまとめると以下の通りです。

正確な小数計算が必要な場面(特に金額)はBigDecimalを使う
BigDecimalは生成方法と比較/丸めでハマりやすい
・new¥BigDecimal("0.1")またはBigDecimal.valueOf(0.1)
・比較はcompareTo
・divideではスケールと丸め指定を忘れない