はじめに
float型とdouble型は主に科学計算や工学計算を目的として設計されています。これらはバイナリ浮動小数点演算を実行し、広範な数値範囲においてある程度正確な高速近似計算を行うように工夫されています。しかし、完全に正確な結果を提供しないため、精度が求められる場面では使用すべきではありません。一方で、商業計算ではしばしば正確な結果が必要となるため、BigDecimalクラスがその役割を果たします。
問題の具体例
以下のコードをご覧ください:
public static void main(String[] args) {
System.out.println(0.2 + 0.1);
System.out.println(0.3 - 0.1);
System.out.println(0.2 * 0.1);
System.out.println(0.3 / 0.1);
}
実行結果は予期せぬものになります。なぜこのような現象が起こるのでしょうか?理由はコンピュータが2進数で動作していることにあります。浮動小数点数は2進数で正確に表現できません。CPUが浮動小数点数を表すのは指数部と仮数部から構成され、このような表現方法では必ずしも精度が保証されず、一部の浮動小数点演算では誤差が生じます。例えば、2.4の2進表現は正確な2.4ではなく、最も近い2進表現は2.3999999999999999です。浮動小数点の値は特定の数学式によって計算されます。
Javaのfloatは科学計算や工学計算用であり、多くの商用計算ではjava.math.BigDecimalクラスを使用して正確な計算を行います。
BigDecimalのコンストラクタ
- public BigDecimal(double val) double型の表現をBigDecimalに変換する(非推奨)
- public BigDecimal(int val) int型の表現をBigDecimalに変換する
- public BigDecimal(String val) String型の表現をBigDecimalに変換する
double型コンストラクタの問題点
なぜ最初のコンストラクタが推奨されないのでしょうか?例を見てみましょう:
public static void main(String[] args) {
BigDecimal numberInteger = new BigDecimal(2);
BigDecimal numberDouble = new BigDecimal(2.3);
BigDecimal numberString = new BigDecimal("2.3");
System.out.println("numberInteger=" + numberInteger);
System.out.println("numberDouble=" + numberDouble);
System.out.println("numberString=" + numberString);
}
JDKの説明
- double型をパラメータとするコンストラクタの結果には一定の予測不可能性があります。Javaでnew BigDecimal(0.1)と記述することで作成されるBigDecimalが0.1(非スケール値1、スケール1)と完全に等しいと考える人もいますが、実際には0.1000000000000000055511151231257827021181583404541015625と等しくなります。これは0.1がdoubleとして正確に表現できないためです。このため、コンストラクタに渡される値は表面的には等しいにもかかわらず、正確に0.1とはなりません。
- 一方で、Stringコンストラクタは完全に予測可能です。new BigDecimal("0.1")と記述することで期待通り0.1と完全に等しいBigDecimalが作成されます。したがって、通常はStringコンストラクタを優先使用することを推奨します。
doubleをBigDecimalのソースとして使用しなければならない場合、
`Double.toString(double)`で文字列に変換し、その後Stringコンストラクタを使用するか、BigDecimalの静的メソッドvalueOfを使用してください:
public static void main(String[] args) {
BigDecimal numberDouble1 = BigDecimal.valueOf(2.3);
BigDecimal numberDouble2 = new BigDecimal(Double.toString(2.3));
System.out.println("numberDouble1=" + numberDouble1);
System.out.println("numberDouble2=" + numberDouble2);
}
BigDecimalの四則演算
一般的な足し算、引き算、掛け算、割り算に対して、BigDecimalクラスは対応するメソッドを提供しています。
public BigDecimal addition(BigDecimal value); //足し算
public BigDecimal subtraction(BigDecimal value); //引き算
public BigDecimal multiplication(BigDecimal value); //掛け算
public BigDecimal division(BigDecimal value); //割り算
基本的な使用方法:
public static void main(String[] args) {
BigDecimal operandA = new BigDecimal("4.5");
BigDecimal operandB = new BigDecimal("1.5");
System.out.println("operandA + operandB =" + operandA.addition(operandB));
System.out.println("operandA - operandB =" + operandA.subtraction(operandB));
System.out.println("operandA * operandB =" + operandA.multiplication(operandB));
System.out.println("operandA / operandB =" + operandA.division(operandB));
}
割り算操作divisonについて注意点があります。
BigDecimalの割り算では割り切れない場合があり、例えば4.5/1.3のようなケースではjava.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.というエラーが発生します。
実際、divisionメソッドには3つのパラメータを渡すことができます:
public BigDecimal division(BigDecimal divisor, int scale, int roundingMode)
第1引数は除数、第2引数は小数点以下保持桁数、第3引数は丸めモードです。
丸めモードは割り算や四捨五入を行う場合にのみ使用され、以下の種類があります:
- ROUND_UP:ゼロから離れる方向への丸め。非ゼロ部分を切り捨て、隣接する桁を1増やす。
- ROUND_DOWN:ゼロに近づく方向への丸め。非ゼロ部分を切り捨て、隣接する桁を増やさない。
- ROUND_CEILING:正の無限大方向への丸め。正数の場合はROUND_UPと同じ、負数の場合はROUND_DOWNと同じ。
- ROUND_FLOOR:負の無限大方向への丸め。正数の場合はROUND_DOWNと同じ、負数の場合はROUND_UPと同じ。
- ROUND_HALF_UP:「最も近い」数字への丸め。2つの隣接数との距離が等しい場合は切り上げ。切り捨て部分>=0.5の場合はROUND_UPと同じ、それ以外はROUND_DOWNと同じ。いわゆる「四捨五入」。
- ROUND_HALF_DOWN:「最も近い」数字への丸め。2つの隣接数との距離が等しい場合は切り捨て。切り捨て部分>0.5の場合はROUND_UPと同じ、それ以外はROUND_DOWNと同じ。「五捨六入」。
- ROUND_HALF_EVEN:「最も近い」数字への丸め。2つの隣接数との距離が等しい場合は偶数方向へ丸める。切り捨て部分の左側の数字が奇数ならROUND_HALF_UPと同じ、偶数ならROUND_HALF_DOWNと同じ。連続した計算で累積誤差を最小限に抑える。いわゆる「銀行家丸め」。
- ROUND_UNNECESSARY:正確な結果が得られることを前提とし、丸め不要を示す。正確な結果を得られない操作にこのモードを指定するとArithmeticExceptionがスローされる。
必要に応じて適切な第3引数を渡してください。四捨五入にはROUND_HALF_UPを使用します。
BigDecimalの切り詰めや四捨五入にはsetScaleメソッドを使用します:
public static void main(String[] args) {
BigDecimal targetNumber = new BigDecimal("4.5635");
targetNumber = targetNumber.setScale(3, RoundingMode.HALF_UP); //3桁小数を保持し、四捨五入
System.out.println(targetNumber);
}
減算・乗算・除算はすべて新しいBigDecimalオブジェクトを返します。BigIntegerとBigDecimalはどちらも不変(immutable)であり、各計算ステップで常に新しいオブジェクトが生成されます。
public static void main(String[] args) {
BigDecimal operandA = new BigDecimal("4.5");
BigDecimal operandB = new BigDecimal("1.5");
operandA.addition(operandB);
System.out.println(operandA); //4.5が出力される。元のoperandAは変化しない
}
BigDecimalの比較
public static void main(String[] args) {
BigDecimal firstValue = new BigDecimal(101);
BigDecimal secondValue = new BigDecimal(111);
//compareToメソッドを使用
//注意:firstValue、secondValueはnullであってはならない
if (firstValue.compareTo(secondValue) == -1) {
System.out.println("firstValueはsecondValueより小さい");
}
if (firstValue.compareTo(secondValue) == 0) {
System.out.println("firstValueはsecondValueと等しい");
}
if (firstValue.compareTo(secondValue) == 1) {
System.out.println("firstValueはsecondValueより大きい");
}
if (firstValue.compareTo(secondValue) > -1) {
System.out.println("firstValueはsecondValue以上");
}
if (firstValue.compareTo(secondValue) < 1) {
System.out.println("firstValueはsecondValue以下");
}
}
BigDecimalからStringへの変換
public static void main(String[] args) {
// 浮動小数点の表示
System.out.println(new BigDecimal("10000000000").toString());
// 通常の数字文字列
System.out.println(new BigDecimal("100.000").toString());
// 末尾の余分な0を削除
System.out.println(new BigDecimal("100.000").stripTrailingZeros().toString());
// 科学的表記法を避ける
System.out.println(new BigDecimal("100.000").stripTrailingZeros().toPlainString());
}
表示結果:
- toString()メソッドは通常の数字文字列を出力します。
- stripTrailingZeros()関数は末尾の余分な0を削除します。
- toPlainString()関数はtoString()の代わりに使用し、科学的表記法の文字列を避けることができます。
まとめ
- 商用計算ではBigDecimalを使用してください。
- String型のパラメータを持つコンストラクタの使用を推奨します。
- BigDecimalは不変(immutable)であるため、各計算ステップで新しいオブジェクトが生成されます。四則演算を行う際は計算後の値を確実に保存してください。