javaで数値の計算を行うときに計算結果がずれる経験はないだろうか.int型とint型の足し算や引き算はうまくいくのに,doble型の計算を行うとずれる場合がある.本記事ではその原因の解説と,BigDecimalを用いた正しい計算方法を解説する.
javaでの計算がうまくいかない理由
浮動小数点を下記の計算結果を見ていただきたい.
double num1 = 0.5;
double num2 = 0.25;
double sum1 = num1 + num2;
System.out.println("num1 + num2 = " + sum1);
double num3 = 0.8;
double num4 = 0.9;
double sum2 = num3 + num4;
System.out.println("num3 + num4 = " + sum2);
// 実行結果
num1 + num2 = 0.75
num3 + num4 = 1.7000000000000002
?????? 1.7000000000000002 ??????
最初は正しい値が計算できているのに,次に計算した方は変な数値になっている!
なぜ1.7と計算できないかと言うとdobleには必ず誤差が存在する.doble型での計算をする場合は必ずこの誤差を考慮しなければならない.
なぜ誤差が出るのか?
上記の例で見た0.5と0.25と0.8と0.9を2進数に変換する.
0.8 -> 0.110011001100110011001100.....
0.9 -> 0.111001100110011001100110.....
0.5 -> 0.1
0.25 -> 0.01
0.8や0.9は同じパターンで循環する少数を循環小数といい,0.5や0.25のように循環しない少数を有限小数といいます.dobleが混入している計算をする場合,循環小数が含まれているとこの循環少数の部分が計算しきれなくなる,これを2進数から10進数に戻すときにdoble型の範囲で丸め込まれるため誤差が発生する.
詳しい誤差に関しては下記サイトがわかりやすいです.この記事では誤差に関しては詳しく説明しません.
doubleは誤差と一蓮托生
https://www.bold.ne.jp/engineer-club/java-double#-double-2
Javaの計算はBigDecimalを使え!!
上述した誤差によって計算が食い違わないようにjavaでの計算はBigDecimalを使いましょう.先程の計算をBigDecimalで計算し直します.
double num1 = 0.8;
double num2 = 0.9;
BigDecimal bigDecimal1 = BigDecimal.valueOf(num1);
BigDecimal bigDecimal2 = BigDecimal.valueOf(num2);
double sum = bigDecimal1.add(bigDecimal2).doubleValue();
System.out.println("num1 + num2 = " + sum);
まずvalueOfメソッドでdoble型の変数の値をBigDecimal型に変換しています.次にaddでbigDecimal1とbigDecimal2の和を計算しています.実行結果は下記になります.
// 実行結果
num1 + num2 = 1.7
誤差を出すことなく正しい値が計算できます.
BigDecimalの構成と名称
BigDecimalは符号と小数点以下の精度を自由に決定できる10進数の値です.またBigDecimalは任意精度なしの符号付き整数部と32ビットの整数のスケールで構成されます.
ここで言うスケールとは小数以下の桁数のことを指します.javaのドキュメントスケールと呼んでおり,四捨五入や切り捨て,切り上げ等のオプションを指定して計算できます.
BigDecimalの四則演算
BigDecimalの四則演算では下記のメソッドを用いて計算を行います.このメソッド以外に,スケールの丸め込み設定を同時に行えるメソッドがありますが,特に指定しなければデフォルトのスケール計算が行われます.
四則演算 | メソッド | デフォルトのスケール計算 |
---|---|---|
加算 + | add(BigDecimal augend) | max(addend.scale(), augend.scale()) |
減算 – | subtract(BigDecimal subtrahend) | max(minuend.scale(), subtrahend.scale()) |
乗算 × | multiply(BigDecimal multiplicand) | multiplier.scale() + multiplicand.scale() |
除算 ÷ | divide(BigDecimal divisor) | dividend.scale() – divisor.scale() |
加算のスケール計算
maxになっているため2つの値の少数の桁が大きい方に合わせます.
例)0.12 + 0.4567 = 0.5767 (max(2,4)=4)
減算のスケール計算
maxになっているため2つの値の少数の桁が大きい方に合わせます.
例)0.1 – 0.010 = 0.090 (max(2,4)=4)
乗算のスケール計算
2つのスケールの足し算になります
例)0.2 × 0.02 = 0.004 (1+2=3)
除算のスケール計算
除算する値のスケールから除算対象のスケールの差になります.
除算は無限小数となる可能性がある.上述のdivide(BigDecimal divisor)は正確な値を期待するメソッドだが,割り切ることができない場合,ArithmeticExceptionがスローされる.
このexceptionを防ぐためには後述する丸め込みモードと桁数の指定をする必要がある
例1)0.3 ÷ 0.25 = 1.8 (2-1=1)
例2)1 ÷ 3 = 0.3333 ArithmeticException
少数の丸めモード(RoundingMode)
四則演算のメソッドには丸め込みモードを設定できる.丸め込みーモードの一覧は以下になります.
列挙型の定数名 | 説明 | 例 |
---|---|---|
DOWN | 0に近づくように丸める | 小数点第1位で丸める場合: 1.4 → 1 2.7 → 2 |
UP | 0から離れるように丸める | 小数点第1位で丸める場合: 1.4 → 2 2.7 → 3 |
CEILING | 正の無限大に近づけるように丸める.正の値の場合UPと同等で,負の値の場合DOWNと同等になる. | 小数点第1位で丸める場合: 1.4 → 2 -2.7 → -2 |
FLOOR | 負の無限大に近づけるように丸める.正の値の場合DOWNと同等で,負の値の場合UPの同等になる. | 小数点第1位で丸める場合: 1.4 → 1 -2.7 → -3 |
HALF_UP | 最も近い数値に丸め込む.両隣が同等の距離の場合切り上げ. 0.5以上なUPと同等,それ以外はDOWNと同等 | 小数点第1位で丸める場合: 1.5 → 2 1.4 → 1 -2.5 → -3 -2.4 → -2 |
HALF_DOWN | 最も近い数値に丸め込む.両隣が同等の距離の場合切り捨て. 0.5を超える場合はUPと同等,それ以外はDOWNと同等 | 小数点第1位で丸める場合: 1.6 → 2 1.5 → 1 -2.6 → -3 -2.5 → -2 |
HALF_EVEN | 最も近い数値に丸め込む.両隣が同等の距離の場合偶数側に丸める. 破棄する小数部の左側の桁が奇数の場合はHALF_UPと同等偶数の場合は HALF_DOWNと同等 | 小数点第1位で丸める場合: 5.5 → 6 2.5 → 2 -1.1 → -1 -1.6 → -2 |
UNNECESSARY | 演算結果が無限小数でなく,正確な値を返すため丸めが必要ないときに使用する | 1 → 1 -1 → -1 -1.1 → ArithmeticExceptionをスロー |
上記の丸め込みモードを適用した四則演算を行ってみます.
加算の丸め込みモードを加味した計算
MathContextには,どの小数点で丸め込みを行うかとそのモードが入っています.これをadd()の第二引数に設定します.
double num1 = 0.812;
double num2 = 0.91;
BigDecimal bigDecimal1 = BigDecimal.valueOf(num1);
BigDecimal bigDecimal2 = BigDecimal.valueOf(num2);
MathContext mathContext = new MathContext(2, RoundingMode.DOWN);
double sum = bigDecimal1.add(bigDecimal2, mathContext).doubleValue();
System.out.println("num1 + num2 = " + sum);
上記の場合切り捨てになるので結果は以下のようになります.
num1 + num2 = 1.7
MathContext()のコンストラクタの第一引数に小数第何位でまるマ込みを行うか設定します.
減算の丸め込みモードを加味した計算
加算と同じ指定方法になるため説明は省略します.
MathContext mathContext = new MathContext(2, RoundingMode.UP);
double sum = bigDecimal1.subtract(bigDecimal2, mathContext).doubleValue();
減算の丸め込みモードを加味した計算
加算,減算と同じ指定方法になるため説明は省略します.
MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP);
double sum = bigDecimal1.multiply(bigDecimal2, mathContext).doubleValue();
除算の丸め込みモードを加味した計算
除算は小数が発生し,一番ArithmeticExceptionがスローされる可能性があります.基本的にはRoundingModeを指定するようにしましょう.
divideのメソッドは小数の桁数とRoundingModeを直接指定できます.
MathContext mathContext = new MathContext(2, RoundingMode.HALF_UP);
double sum1 = bigDecimal1.divide(bigDecimal2, mathContext).doubleValue();
// divide(BigDecimal divisor, int scale, int roundingMode)で指定もできる
double sum2 = bigDecimal1.divide(bigDecimal2, 2, RoundingMode.HALF_UP).doubleValue();
BigDecimalは他にも使いやすいメソッドがいくつもあります.本記事では下記のドキュメントの一部を抜粋しております.詳しくは下記サイトまで!
クラス BigDecimal
https://docs.oracle.com/javase/jp/7/api/java/math/BigDecimal.html
クラス MathContext
https://docs.oracle.com/javase/jp/7/api/java/math/MathContext.html