Java : 小数を誤差なしで計算 (BigDecimal)
正確な計算をしたいのに、double や float を使って誤差に悩まされたことはありませんでしょうか?
そんなときには、BigDecimal クラスを使ってみましょう。
本記事では、
- なぜ double や float は誤差が発生するのか?
- BigDecimalを使い 10進数 として正確に計算する
の2つについて解説していきます。
概要
Java で小数(実数) を扱うには、プリミティブ型 の double または float を使うのが一般的です。
double と float の違いは扱える実数の範囲です。double のほうが扱える範囲が広くなります。
しかし、double や float を使って計算すると、思わぬ誤差が発生することがあります。
// 誤差が発生する例です。
final double a = 0.1;
final double b = 0.2;
final double c = a + b;
System.out.println(a); // 0.1
System.out.println(b); // 0.2
System.out.println(c); // 0.30000000000000004
10進数として誤差のない正確な計算をしたい場合は、代わりに BigDecimal を使います。
final var a = new BigDecimal("0.1");
final var b = new BigDecimal("0.2");
final var c = a.add(b);
System.out.println(a); // 0.1
System.out.println(b); // 0.2
System.out.println(c); // 0.3
無事に、誤差なしで計算できました。
なぜ誤差が発生するのか?
double と float は浮動小数点数です。
小さなメモリサイズで、非常に広範囲の数値を表現できるのが特徴です。
浮動小数点数が、具体的にどのように内部データとして数値を表現しているのかは、少々難しくなるので説明は割愛します。
興味のあるかたは、引用元のページに詳細があるのでご参照ください。
ざっくり簡単にいうと、浮動小数点数は数値(小数点以下を含む) を 2進数 で表現して保持しています。
一方、私たち人間は、数値を 10進数 として考えます。
この 2進数と 10進数という違いが、思わぬ誤差を発生させます。
例として、10進数 で表記された 0.1 という数値について考えてみましょう。
final double a = 0.1;
System.out.println(a); // 0.1
変数 a に 0.1 を代入して、それを出力させます。
出力も 0.1 となり、一見、問題ないように見えますね。
しかし、実はすでに誤差が発生しています。
final double a = 0.1;
System.out.println(a); // 0.1
final var dec = new BigDecimal(a);
System.out.println(dec.toPlainString()); // 0.1000000000000000055511151231257827021181583404541015625
BigDecimal に変換してから、より正確な値を表示させると、その誤差が確認できます。
0.1000000000000000055511151 ... という感じで、わずかな誤差が発生していることが分かりますね。
final double a = 0.1;
^^^ <--- リテラル表記
リテラル表記 の 0.1 は 10進数です。
その 0.1 を、浮動小数点数(2進数) に変換(変数 a に代入)するときに誤差が発生するのです。
補足
- 浮動小数点数には、10進数で内部データを表現する形式もあります。
ただ、Java のプリミティブ型では採用されていないので、本記事では割愛します。
0.1 (10進数) は 2進数で正確に表現できない
10進数として表記された 0.1 という数値は、当然、10進数では誤差なしに表現できています。
しかし、0.1(10進数) という数値は、2進数では誤差なしでは表現できません。
2進数の小数表記がちょっとイメージしにくいかもしれないので、簡単に表にしてみました。
例えば、10進数で 0.5 という数値は、2進数で表記すると 0.1 になります。
2進数表記の小数 | 10進数表記 | コード例 |
---|---|---|
0.1 | 0.5 |
|
0.01 | 0.25 |
|
0.001 | 0.125 |
|
0.0001 | 0.0625 |
|
0.00001 | 0.03125 |
|
補足
- リテラル表記 の "0x1p-1" の p は基数2を表します。
"1.23e+3" の e は基数10です。
0.1(10進数) を 2進数 で表現しようとすると
0.0001100110011001100110011 ...
となり、0011 のパターンが永遠と続きます。
このような小数を循環小数と呼びます。
10進数でも誤差なしで表現できない数値はあります。
例えば、1 ÷ 3 は、0.333333 ... となり、10進数では正確に表現できません。(循環小数)
しかし、3進数であれば正確に表現できます。
このように、x進数の違いによって、正確に表現できる数値に違いがあります。
10進数 では正確に表現できる数値なのに、2進数 では正確に表現できない数値が、思わぬ誤差となります。
リテラル表記は 10進数 で、浮動小数点数は 2進数 です。
誤差の発生する理由が、なんとなくイメージできましたでしょうか。
補足
- x進数の違いによる誤差以外に、例えば情報落ちと呼ばれる誤差などもあります。
詳しくは「浮動小数点数 - Wikipedia」をご参照ください。
BigDecimal
BigDecimal は、正確な 10進数 を表すクラスです。
BigDecimal を使うことにより、前述してきた浮動小数点による誤差を発生させずに、10進数 としての正確な計算ができます。
final var a = new BigDecimal("0.1");
final var b = new BigDecimal("0.2");
System.out.println(a); // 0.1
System.out.println(b); // 0.2
// 0.1 + 0.2 を計算します。
final var c = a.add(b);
// 誤差は発生しません。
System.out.println(c); // 0.3
BigDecimal は、スケールなし整数値 と スケール(単位) で構成されます。
それぞれの具体的な値は、下記のメソッドで取得できます。
- スケールなし整数値 : BigDecimal.unscaledValue()
- スケール(単位) : BigDecimal.scale()
スケールなし整数値 と スケール(単位) の関係については、スケール のところでもう少し詳しく解説します。
生成
BigDecimal オブジェクトを生成するには、コンストラクタ か valueOf メソッドを使います。
基本的には、文字列を指定するコンストラクタをおすすめします。
// 文字列から生成すれば誤差は発生しません。
final var a = new BigDecimal("0.1");
final var b = new BigDecimal("0.2");
System.out.println(a); // 0.1
System.out.println(b); // 0.2
double を指定するコンストラクタは、その時点で誤差が発生するのでご注意ください。
final var dec = new BigDecimal(0.1);
System.out.println(dec); // 0.1000000000000000055511151231257827021181583404541015625
BigDecimal.valueOf(double val) も、本質的には誤差が発生します。
(double を文字列に変換する Double.toString(double) メソッドに依存します)
よって、使わない方が無難でしょう。
final var dec = BigDecimal.valueOf(1.234567890123456789);
System.out.println(dec.toPlainString()); // 1.2345678901234567
スケール
スケールは、BigDecimal を使う上で、ぜひ理解しておきたい重要な概念です。
ちょっと分かりづらいので、ゆっくりと理解していきましょう。
スケールが 0 または正の場合、それは小数点以下の桁数を表します。
これは比較的分かりやすいかなと思います。
// 小数点以下がない場合はスケールは 0 です。
final var dec1 = new BigDecimal("1");
System.out.println(dec1); // 1
System.out.println(dec1.scale()); // 0
final var dec2 = new BigDecimal("1234");
System.out.println(dec2); // 1234
System.out.println(dec2.scale()); // 0
// 小数点以下1桁の例です。(スケール1)
final var dec3 = new BigDecimal("1.2");
System.out.println(dec3); // 1.2
System.out.println(dec3.scale()); // 1
// 小数点以下2桁の例です。(スケール2)
final var dec4 = new BigDecimal("1.23");
System.out.println(dec4); // 1.23
System.out.println(dec4.scale()); // 2
次は、スケールが負の場合です。
API仕様によると、
- スケールの正負を逆にした値を指数とする10の累乗を乗算します。
とあります。
…ちょっと分かりにくいですよね。
別の観点で考えてみましょう。
スケールは 単位 と考えることもできます。
単位とは、1 という値がどれくらいの量であるかを定義したものです。
例えば、1 km (キロメートル) は 1000 m (メートル) ですね。
123 km なら 123 × 1000 = 123000 m となります。
BigDecimal は スケールなし整数値 と スケール(単位) で構成されます。
そして、単位は
10-scale
となります。
例えば、スケールが 2 の場合、単位は
10-2 = 0.01
となります。
もしスケールなし整数値が 123 であれば、単位が 0.01 なので、
123 × 0.01 = 1.23
となります。
このように、BigDecimalは整数値とスケールで小数を表現します。
// スケールなし整数値 123、スケール 2 で生成します。
final var dec = BigDecimal.valueOf(123, 2);
System.out.println(dec); // 1.23
スケールを負にした場合も考え方は同じです。
例えば、スケールが -2 の場合、単位は
10-(-2) = 102 = 100
となります。
スケールなし整数値が 123 であれば、単位が 100 なので、
123 × 100 = 12300
となります。
// スケールなし整数値 123、スケール -2 で生成します。
final var dec = BigDecimal.valueOf(123, -2);
System.out.println(dec.toPlainString()); // 12300
使用例
それではスケールを使った演算の例を見てみましょう。
よく使われるケースとしては、
- 割り算で、小数点以下 x桁まで計算
でしょうか。
// 98 ÷ 8 = 12.25
final var dec = new BigDecimal("98");
System.out.println(dec); // 98
final var divisor = new BigDecimal("8");
System.out.println(divisor); // 8
// スケールを指定しない例です。
final var ret1 = dec.divide(divisor);
System.out.println(ret1); // 12.25
System.out.println(ret1.unscaledValue()); // 1225
System.out.println(ret1.scale()); // 2
// スケール 1 、丸めモード HALF_UP の例です。
final var ret2 = dec.divide(divisor, 1, RoundingMode.HALF_UP);
System.out.println(ret2); // 12.3
System.out.println(ret2.unscaledValue()); // 123
System.out.println(ret2.scale()); // 1
演算によって結果のスケールが変わる
BigDecimal で演算すると、結果の BigDecimal オブジェクトは必要に応じてスケールが変わります。
基本的にはよい感じにスケールを拡張してくれるので、気にしなくてもよいとは思います。
// 4.0 × 1.5 = 6.00
final var dec = new BigDecimal("4.0");
System.out.println(dec); // 4.0
System.out.println(dec.unscaledValue()); // 40
System.out.println(dec.scale()); // 1
final var multiplicand = new BigDecimal("1.5");
System.out.println(multiplicand); // 1.5
System.out.println(multiplicand.unscaledValue()); // 15
System.out.println(multiplicand.scale()); // 1
// スケールが 2 に拡張されます。
final var ret = dec.multiply(multiplicand);
System.out.println(ret); // 6.00
System.out.println(ret.unscaledValue()); // 600
System.out.println(ret.scale()); // 2
どのようなルールで拡張されるのかは、API仕様に詳細がありますのでそちらをご確認ください。
また、後述する 精度 の指定によってもスケールが変わります。
// 2 + 0.345 = 2.345
final var dec = new BigDecimal("2");
System.out.println(dec); // 2
System.out.println(dec.unscaledValue()); // 2
System.out.println(dec.scale()); // 0
final var augend = new BigDecimal("0.345");
System.out.println(augend); // 0.345
System.out.println(augend.unscaledValue()); // 345
System.out.println(augend.scale()); // 3
// 精度を指定しない例です。
final var ret1 = dec.add(augend);
System.out.println(ret1); // 2.345
System.out.println(ret1.unscaledValue()); // 2345
System.out.println(ret1.scale()); // 3
// 精度を2桁にする例です。
final var ret2 = dec.add(augend, new MathContext(2));
System.out.println(ret2); // 2.3
System.out.println(ret2.unscaledValue()); // 23
System.out.println(ret2.scale()); // 1
演算
BigDecimal の演算は、10進数として正確な結果を返します。
1 ÷ 3 のような割り切れない演算でも、例外(ArithmeticException) が発生するので安心です。
とにかく大事なのは、意図していない丸め誤差が発生しない ということです。
もし意図して結果を丸めたい場合は、後述する「丸めモード」や「精度」を使いましょう。
足し算
足し算は add メソッドを使います。
final var dec = new BigDecimal("100");
final var augend = new BigDecimal("23");
final var ret = dec.add(augend);
System.out.println(ret); // 123
final var dec = new BigDecimal("100");
final var augend = new BigDecimal("-23");
final var ret = dec.add(augend);
System.out.println(ret); // 77
final var dec = new BigDecimal("0.1");
final var augend = new BigDecimal("0.023");
final var ret = dec.add(augend);
System.out.println(ret); // 0.123
引き算
引き算は subtract メソッドを使います。
final var dec = new BigDecimal("100");
final var subtrahend = new BigDecimal("23");
final var ret = dec.subtract(subtrahend);
System.out.println(ret); // 77
final var dec = new BigDecimal("100");
final var subtrahend = new BigDecimal("-23");
final var ret = dec.subtract(subtrahend);
System.out.println(ret); // 123
final var dec = new BigDecimal("0.1");
final var subtrahend = new BigDecimal("0.023");
final var ret = dec.subtract(subtrahend);
System.out.println(ret); // 0.077
掛け算
掛け算は multiply メソッドを使います。
// 100 × 23
final var dec = new BigDecimal("100");
final var multiplicand = new BigDecimal("23");
final var ret = dec.multiply(multiplicand);
System.out.println(ret); // 2300
// 100 × -23
final var dec = new BigDecimal("100");
final var multiplicand = new BigDecimal("-23");
final var ret = dec.multiply(multiplicand);
System.out.println(ret); // -2300
// 0.123 × 0.1
final var dec = new BigDecimal("0.123");
final var multiplicand = new BigDecimal("0.1");
final var ret = dec.multiply(multiplicand);
System.out.println(ret); // 0.0123
割り算
割り算は、四則演算の中では一番デリケートな演算です。
1 ÷ 3 など割り切れない(10進数で表現できない) 演算となる可能性があるためです。
// 6 ÷ 2
final var dec = new BigDecimal("6");
final var divisor = new BigDecimal("2");
final var ret = dec.divide(divisor);
System.out.println(ret); // 3
// 5 ÷ 4
final var dec = new BigDecimal("5");
final var divisor = new BigDecimal("4");
final var ret = dec.divide(divisor);
System.out.println(ret); // 1.25
割り切れない場合は、例外(ArithmeticException) が発生します。
// 10 ÷ 3
final var dec = BigDecimal.TEN;
final var divisor = new BigDecimal("3");
try {
final var ret = dec.divide(divisor);
} catch (ArithmeticException e) {
System.out.println("ArithmeticException! : " + e.getMessage());
}
// 結果
// ↓
//ArithmeticException! : Non-terminating decimal expansion; no exact representable decimal result.
小数点以下 x桁で丸めたい場合は、スケールと 丸めモード を指定します。
// 10 ÷ 3
final var dec = BigDecimal.TEN;
final var divisor = new BigDecimal("3");
// 小数点以下 5桁で切り捨てます。
final var ret = dec.divide(divisor, 5, RoundingMode.FLOOR);
System.out.println(ret); // 3.33333
System.out.println(ret.scale()); // 5
丸めモード
数値を丸めるには、RoundingMode 列挙型を使います。
厳密にはちょっと違うかもしれませんが、イメージしやすく言うと、切り上げ・切り捨て・四捨五入などです。
丸めモード | 説明 |
---|---|
CEILING | 正の無限大に近づくように丸めるモードです。 |
DOWN | 0に近づくように丸めるモードです。 |
FLOOR | 負の無限大に近づくように丸めるモードです。 |
HALF_DOWN | 「もっとも近い数字」に丸める丸めモードです(両隣りの数字が等距離の場合は切り捨てます)。 |
HALF_EVEN | 「もっとも近い数字」に丸める丸めモードです(ただし、両隣りの数字が等距離の場合は偶数側に丸めます)。 |
HALF_UP | 「もっとも近い数字」に丸める丸めモードです(ただし、両隣りの数字が等距離の場合は切り上げます)。 |
UNNECESSARY | 要求される演算の結果が正確であり、丸めが必要でないことを表す丸めモードです。 |
UP | 0から離れるように丸めるモードです。 |
割り算(divide) で指定したり、直接 BigDecimal.round メソッドで丸めることもできます。
// 10 ÷ 3 = 3.33333 ... の例です。
final var dec = BigDecimal.TEN;
final var divisor = new BigDecimal("3");
// 小数点以下 2桁で丸めます。
// 切り上げ(正の無限大に近づくように丸めます)
final var ret1 = dec.divide(divisor, 2, RoundingMode.CEILING);
System.out.println(ret1); // 3.34
// 切り捨て(負の無限大に近づくように丸めます)
final var ret2 = dec.divide(divisor, 2, RoundingMode.FLOOR);
System.out.println(ret2); // 3.33
// 値を直接丸める例です。
final var dec = new BigDecimal("1111.5");
// 4桁になるように丸めます。
final var ret1 = dec.round(new MathContext(4, RoundingMode.CEILING));
System.out.println(ret1); // 1112
final var ret2 = dec.round(new MathContext(4, RoundingMode.FLOOR));
System.out.println(ret2); // 1111
また、UNNECESSARY を使うことにより、丸めを許さない厳格な演算ができます。
丸めないと表現できない数値…例えば 10 ÷ 3 などの割り切れない演算は、例外(ArithmeticException) が発生します。
// 10 ÷ 3 の例です。
final var dec = BigDecimal.TEN;
final var divisor = new BigDecimal("3");
try {
final var ret = dec.divide(divisor, 2, RoundingMode.UNNECESSARY);
} catch (ArithmeticException e) {
System.out.println("ArithmeticException! : " + e.getMessage());
}
// 結果
// ↓
//ArithmeticException! : Rounding necessary
精度
各種演算(add、subtract、multiply、divide) で何桁まで計算するか?を指定するのに使うのが精度です。
MathContext で丸めモードとセットで使われます。
もう少し具体的にいうと、精度とは、スケールなし整数値(BigDecimal.unscaledValue) の桁数です。
111 + 0.9 を計算する例です。
// 精度を指定しない例です。(スケールは 1 になります)
final var dec = new BigDecimal("111");
final var augend = new BigDecimal("0.9");
final var ret = dec.add(augend);
System.out.println(ret); // 111.9
// スケールなしの整数値とスケール
System.out.println(ret.unscaledValue()); // 1119
System.out.println(ret.scale()); // 1
// 精度を 3 に指定する例です。(スケールは 0 になります)
final var dec = new BigDecimal("111");
final var augend = new BigDecimal("0.9");
final var ret = dec.add(augend, new MathContext(3, RoundingMode.CEILING));
System.out.println(ret); // 112
// スケールなしの整数値とスケール
System.out.println(ret.unscaledValue()); // 112
System.out.println(ret.scale()); // 0
// 精度を 2 に指定する例です。(スケールは -1 になります)
final var dec = new BigDecimal("111");
final var augend = new BigDecimal("0.9");
final var ret = dec.add(augend, new MathContext(2, RoundingMode.CEILING));
System.out.println(ret.toPlainString()); // 120
// スケールなしの整数値とスケール
System.out.println(ret.unscaledValue()); // 12
System.out.println(ret.scale()); // -1
また、MathContext.UNLIMITED を使うと桁数を無制限に計算します。
ただし、1 ÷ 3 のような割り切れない演算など、桁数が無限となる場合は例外(ArithmeticException) が発生します。
// 割り切れない演算の精度 5 の例です。
final var divisor = new BigDecimal("3");
// 1 ÷ 3
final var ret = BigDecimal.ONE.divide(divisor, new MathContext(5));
System.out.println(ret); // 0.33333
// スケールなしの整数値とスケール
System.out.println(ret.unscaledValue()); // 33333
System.out.println(ret.scale()); // 5
// 割り切れない演算の精度無制限の例です。
final var divisor = new BigDecimal("3");
try {
// 1 ÷ 3
final var ret = BigDecimal.ONE.divide(divisor, MathContext.UNLIMITED);
} catch (ArithmeticException e) {
System.out.println("ArithmeticException! : " + e.getMessage());
}
// 結果
// ↓
//ArithmeticException! : Non-terminating decimal expansion; no exact representable decimal result.
まとめ
浮動小数点数である double と float は思わぬ誤差が発生することがあります。
理由としては、
- 浮動小数点数は数値(小数点以下を含む) を 2進数 で表現して保持している
- 10進数 で正確に表現できる数値が、2進数 (浮動小数点数) では正確に表現できないことがある
という点です。
もし 10進数 として誤差なしで計算したい場合は BigDecimal を使いましょう。
多少の誤差は許容できて、高速に計算したい場合は浮動小数点数(double, float) を使いましょう。
用途によって使い分けることが大事ですね。
BigDecimal については、紹介しきれていないAPIもたくさんあるので、下記の API使用例の記事も参考にしていただけたら幸いです。