広告

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)は、実数をコンピュータで処理(演算や記憶、通信)するために有限桁の小数で近似値として扱う方式であり[1]、コンピュータの数値表現として広く用いられている。

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進数の小数表記がちょっとイメージしにくいかもしれないので、簡単に表にしてみました。
例えば、10進数で 0.5 という数値は、2進数で表記すると 0.1 になります。

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進数 です。

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

補足

  • x進数の違いによる誤差以外に、例えば情報落ちと呼ばれる誤差などもあります。
    詳しくは「浮動小数点数 - Wikipedia」をご参照ください。

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に変換します。

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 を使う上で、ぜひ理解しておきたい重要な概念です。
ちょっと分かりづらいので、ゆっくりと理解していきましょう。

スケールが 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) が発生するので安心です。

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

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

足し算

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進数で表現できない) 演算となる可能性があるためです。

// 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

精度

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

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

// 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.

まとめ

浮動小数点数である doublefloat は思わぬ誤差が発生することがあります。

理由としては、

  • 浮動小数点数は数値(小数点以下を含む) を 2進数 で表現して保持している
  • 10進数 で正確に表現できる数値が、2進数 (浮動小数点数) では正確に表現できないことがある

という点です。

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

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

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


関連記事

ページの先頭へ