Java : 小数を誤差なしで計算 (BigDecimal)

正確な計算をしたいのに、doublefloat を使って誤差に悩まされたことはありませんでしょうか?
そんなときには、BigDecimalクラスを使ってみましょう。

本記事では、

  • なぜ doublefloat は誤差が発生するのか?
  • BigDecimalを使い 10進数 として正確に計算する

の2つについて解説していきます。


概要

4.2.3. Floating-Point Types and Values
The floating-point types are float and double, which are conceptually associated with the 32-bit binary32 and 64-bit binary64 floating-point formats for IEEE 754 values and operations, as specified in the IEEE 754 Standard (§1.7).

Javaで小数(実数)を扱うには、プリミティブ型の double または float を使うのが一般的です。
doublefloat の違いは扱える実数の範囲の違いです。double のほうが扱える範囲が広くなります。

しかし、doublefloat を使って計算すると、思わぬ誤差が発生することがあります。

// 誤差が発生する例です。
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

無事に、誤差なしで計算できました。

なぜ誤差が発生するのか?

浮動小数点数(ふどうしょうすうてんすう、英: floating point number)は、浮動小数点方式と呼ばれる方式によって表現された数のことである。浮動小数点方式においては、固定長の仮数部と固定長の指数部の2つの部分の組み合わせによって、数値を表現する。

doublefloat は浮動小数点数です。
小さなメモリサイズで、非常に広範囲の数値を表現できるのが特徴です。

浮動小数点数が、具体的にどのように内部データとして数値を表現しているのかは、少々難しくなるので説明は割愛します。
興味のあるかたは、引用元のページに詳細があるのでご参照ください。

ざっくり簡単にいうと、浮動小数点数は 数値(小数点以下を含む)を 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進数の小数表記がちょっとイメージしにくいかもしれないので、簡単に表にしてみました。

2進数表記の小数 10進数表記 コード例
0.1 0.5
final double d = 0x1p-1;
System.out.println(new BigDecimal(d).toPlainString()); // 0.5
0.01 0.25
final double d = 0x1p-2;
System.out.println(new BigDecimal(d).toPlainString()); // 0.25
0.001 0.125
final double d = 0x1p-3;
System.out.println(new BigDecimal(d).toPlainString()); // 0.125
0.0001 0.0625
final double d = 0x1p-4;
System.out.println(new BigDecimal(d).toPlainString()); // 0.0625
0.00001 0.03125
final double d = 0x1p-5;
System.out.println(new BigDecimal(d).toPlainString()); // 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進数であれば正確に表現できます。

BigDecimal構成

このように、x進数の違いによって、正確に表現できる数値に違いがあります。

10進数では正確に表現できる数値なのに、2進数では正確に表現できない数値が、思わぬ誤差となります。
リテラル表記は 10進数 で、浮動小数点数は 2進数です。

誤差の発生する理由が、なんとなくイメージできましたでしょうか。

BigDecimal

変更が不可能な、任意精度の符号付き10進数です。 BigDecimalは、任意の精度整数「スケールなしの値」および32ビットの整数scaleで構成されます。

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は、スケールなし整数値とスケール(単位)で構成されます。

それぞれの具体的な値は、下記のメソッドで取得できます。

  • スケールなし整数値 : BigDecimal.unscaledValue()
  • スケール(単位) : BigDecimal.scale()

スケールなし整数値とスケール(単位)の関係については、スケール のところでもう少し詳しく解説します。

生成

public BigDecimal​(String val)
BigDecimalの文字列表現をBigDecimalに変換します。 文字列表現は、任意の符号「+」(「\u002B」)または「-」(「\u002D」)と、それに続く0桁以上の10進数字(「整数部」)の列で構成され、任意で小数部または指数が付随します。

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

スケール

0または正の場合、スケールは小数点以下の桁数です。 負の場合、スケールなしの数値に、スケールの正負を逆にした値を指数とする10の累乗を乗算します。 つまり、BigDecimalで表される数値は(unscaledValue×10-scale)です。

スケール構成

スケールは、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オブジェクトは必要に応じてスケールが変わります。
基本的にはよい感じにスケールを拡張してくれるので、気にしなくてもよいとは思います。

// 5 ÷ 4 = 1.25

final var dec = new BigDecimal("5");
System.out.println(dec); // 5
System.out.println(dec.unscaledValue()); // 5
System.out.println(dec.scale()); // 0

final var divisor = new BigDecimal("4");
System.out.println(divisor); // 4
System.out.println(divisor.unscaledValue()); // 4
System.out.println(divisor.scale()); // 0

// スケールが 0 → 2 に拡張されます。
final var ret = dec.divide(divisor);
System.out.println(ret); // 1.25
System.out.println(ret.unscaledValue()); // 125
System.out.println(ret.scale()); // 2

どのようなルールで拡張されるのかは、API仕様に詳細がありますのでそちらをご確認ください。
一部、ルールを抜粋します。

演算 優先される結果のスケール
加算 max(addend.scale(), augend.scale())
減算 max(minuend.scale(), subtrahend.scale())
乗算 multiplier.scale() + multiplicand.scale()
除算 dividend.scale() - divisor.scale()
平方根 radicand.scale()/2

また、後述する精度の指定によってもスケールが変わります。

// 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) が発生するので安心です。

とにかく大事なのは、意図していない丸め誤差が発生しない ということです。

もし意図して結果を丸めたい場合は、後述する「丸めモード」や「精度」を使いましょう。

足し算

public BigDecimal add(BigDecimal augend)
値が(this+augend)でスケールがmax(this.scale(), augend.scale())であるBigDecimalを返します。

足し算は 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

引き算

public BigDecimal subtract(BigDecimal subtrahend)
値が(this - subtrahend)でスケールがmax(this.scale(), subtrahend.scale())であるBigDecimalを返します。

引き算は 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

掛け算

public BigDecimal multiply(BigDecimal multiplicand)
値が(this×multiplicand)でスケールが(this.scale()+multiplicand.scale())であるBigDecimalを返します。

掛け算は 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

割り算

public BigDecimal divide(BigDecimal divisor)
値が(this /divisor)で優先スケールが(this.scale() - divisor.scale())であるBigDecimalを返します。(小数点以下が無限となるため)正確な商を表現できない場合、ArithmeticExceptionがスローされます。

割り算は、四則演算の中では一番デリケートな演算です。
1 ÷ 3 など割り切れない(10進数で表現できない) 演算となる可能性があるためです。

final var dec = new BigDecimal("6");
final var divisor = new BigDecimal("2");

// 6 ÷ 2
final var ret = dec.divide(divisor);
System.out.println(ret); // 3
final var dec = new BigDecimal("5");
final var divisor = new BigDecimal("4");

// 5 ÷ 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");

//ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
//final var ret = dec.divide(divisor);

小数点以下 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");

// ArithmeticException: Rounding necessary
//final var ret1 = dec.divide(divisor, 2, RoundingMode.UNNECESSARY);

精度

通常、丸めモードと精度設定によって、正確な結果に返される桁数よりも多くの桁数(分裂と平方根の場合にはおそらく無限に多く)がある場合、桁数が制限された演算結果の返り方が決定されます。 まず、返される桁の合計数はMathContextのprecision設定で指定されます。これにより、結果の精度が決定されます。 桁数のカウントは、正確な結果のもっとも左の0でない桁から始まります。 丸めモードでは、破棄される桁が返される結果にどのように影響するかが決定されます。

MathContext構成

各種演算(addsubtractmultiplydivide)で何桁まで計算するか?を指定するのに使うのが精度です。
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");

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");

// ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
//final var ret = BigDecimal.ONE.divide(divisor, MathContext.UNLIMITED);

まとめ

浮動小数点数である doublefloat は、ほぼほぼ誤差が発生します。
誤差の発生する理由は、10進数で正確に表現できる数値が、2進数では正確に表現できないことがあるためです。

もし10進数として誤差なしで計算したい場合は BigDecimal を使いましょう。
多少の誤差は許容できて、高速に計算したい場合は浮動小数点数を使いましょう。

用途によって使い分けることが大事ですね。

BigDecimalについては、紹介しきれていないAPIもたくさんあるので、下記のAPI使用例の記事も参考にしていただけたら幸いです。


関連記事

ページの先頭へ