自分が知りたかったBigDecimalの知識を集める記事。なお、基本型を扱う関係上、新規性は多くありません。強いて言えば、2021年でも扱えるってことくらいですかね。
環境
この記事を読んでわかること一覧
- precisionとscale(有効桁数と精度)
- 型宣言
- 足し算
- 引き算
- 掛け算
- 割り算
- 10倍や100倍、または1/10や1/100
- 余り
- 四捨五入等の丸めを行う
- 比較
- ±変換
- 文字列化
- リストのBigDecimalを計算する
- 1.00000を1に丸める
precisionとscale(有効桁数と精度)
BigDecimalを扱うのであれば抑えておくべき内容。
scaleはsetScale
メソッドが存在するが、precisionに関してはコンストラクタか、掛け算や割り算のパラメータMathContext
で設定しなければならない。
(例)
BigDecimal example = BigDecimal.valueOf(100.00001);
example.scale();
example.precision():
BigDecimal example2 = BigDecimal.valueOf(0.00001);
example2.scale();
example.precision();
型宣言
BigDecimalの型宣言をする方法。
valueOf
のstaticファクトリーメソッドを使いましょう。new 時にDouble型を渡すと、意図しない数値になります。
System.out.println(new BigDecimal(123.45));
System.out.println(new BigDecimal("123.45"));
System.out.println(BigDecimal.valueOf(123.45));
また、0, 1, 10に関してはstatic変数が用意されているので、こちらを使用しましょう。
BigDecimal.ONE;
BigDecimal.ZERO;
BigDecimal.TEN;
足し算
add
メソッドを使用します。また、小数点以下の桁数は多い方に合わせます。
(例)1.0 + 1.00 = 2.00
BigDecimal.ONE.add(BigDecimal.ONE);
引き算
subtract
メソッドを使用します。また、小数点以下の桁数は多い方に合わせます。
(例)1.0 - 1.00 = 0.00
BigDecimal.ONE.subtract(BigDecimal.ONE);
掛け算
multiply
メソッドを使用します。
単純に掛け算するタイプのメソッドと、有効桁数を設定して丸め設定をするタイプのメソッドがあります。計算する桁数が膨大なのであまり細かく計算する必要が無い場合は、後者を使用しましょう。
assertThat(
BigDecimal.valueOf(12345.6789)
.multiply(BigDecimal.TEN, new MathContext(2, RoundingMode.DOWN))
.toPlainString()
).isEqualTo("120000");
assertThat(
BigDecimal.valueOf(12345.6789)
.multiply(BigDecimal.TEN)
.toPlainString()
).isEqualTo("123456.7890");
割り算
divide
メソッドを使用します。
単純に割り算するタイプのメソッドと、有効桁数を設定して丸め設定をするタイプのメソッド、精度を設定して丸め設定をするタイプのメソッドがあります。
割り切れないとExceptionが発生するので、必ずRoundingMode
は使用しましょう。
assertThatThrownBy(
() -> BigDecimal.ONE.divide(BigDecimal.valueOf(3))
).isInstanceOfSatisfying(
ArithmeticException.class,
(e) -> e.getMessage().equals("Non-terminating decimal expansion; no exact representable decimal result.")
);
assertThat(
BigDecimal.ONE
.divide(BigDecimal.valueOf(3), 3, RoundingMode.UP)
.toPlainString()
).isEqualTo("0.334");
assertThat(
BigDecimal.ONE
.divide(BigDecimal.valueOf(3), new MathContext(3, RoundingMode.UP))
.toPlainString()
).isEqualTo("0.334");
また、エラーにならないように適宜丸め処理を行う関係上、精度は意識しておきましょう。
精度を増やさないままに丸めてしまうと、意図しない結果になりかねません。
assertThat(
BigDecimal.ONE
.divide(BigDecimal.valueOf(3), RoundingMode.UP)
.toPlainString()
).isEqualTo("1");
assertThat(
BigDecimal.ONE
.setScale(3, RoundingMode.UP)
.divide(BigDecimal.valueOf(3), RoundingMode.UP)
.toPlainString()
).isEqualTo("0.334");
assertThat(
BigDecimal.ONE
.divide(BigDecimal.valueOf(3), 3, RoundingMode.UP)
.multiply(BigDecimal.valueOf(100))
.toPlainString()
).isEqualTo("33.400");
assertThat(
BigDecimal.ONE
.divide(BigDecimal.valueOf(3), 6, RoundingMode.UP)
.multiply(BigDecimal.valueOf(100))
.setScale(3, RoundingMode.UP)
.toPlainString()
).isEqualTo("33.334");
10倍や100倍、または1/10や1/100
scaleByPowerOfTen
メソッドを使用すると、10倍や1/10倍が簡単にできます。
BigDecimal.ONE.scaleByPowerOfTen(1);
BigDecimal.ONE.scaleByPowerOfTen(2);
BigDecimal.ONE.scaleByPowerOfTen(-1);
BigDecimal.ONE.scaleByPowerOfTen(-2);
movePointRight
メソッドで10倍やmovePointLeft
メソッドで1/10倍も簡単にできます。
BigDecimal.TEN.movePointRight(1);
BigDecimal.ONE.movePointLeft(1);
注意点としては、multiply
メソッドでは有効桁数や精度が変わりますが、10倍や1/10倍するメソッドでは精度が変わりません。10倍した場合は問題ないでしょうが、1/10倍した時の結果は注意しましょう。
assertThat(
BigDecimal.ONE.movePointRight(1).toPlainString()
).isEqualTo("10");
assertThat(
BigDecimal.TEN.movePointLeft(1).toPlainString()
).isEqualTo("1.0");
assertThat(
BigDecimal.ONE.movePointLeft(3).toPlainString()
).isEqualTo("0.001");
assertThat(
BigDecimal.ONE.multiply(BigDecimal.TEN).toPlainString()
).isEqualTo("10");
assertThat(
BigDecimal.TEN.divide(BigDecimal.TEN).toPlainString()
).isEqualTo("1");
余り
remainder
メソッドで余りを求めることができます。
assertThat(
BigDecimal.valueOf(10.11)
.remainder(BigDecimal.valueOf(3))
).isEqualTo(BigDecimal.valueOf(1.11));
四捨五入等の丸めを行う
setScale
で意図的に丸めることができます。掛け算や割り算で精度を指定しない場合は、実行対象のBigDecimalの精度が使われます。
ですので、最初と最後にsetScale
をすると、意図を表現しやすいです。処理順序に左右される意図的な丸めを発生させない時に使用します。
assertThat(
BigDecimal.ONE
.setScale(6, RoundingMode.UP)
.divide(BigDecimal.valueOf(3), RoundingMode.UP)
.multiply(BigDecimal.valueOf(100))
.setScale(3, RoundingMode.UP)
.toPlainString()
).isEqualTo("33.334");
比較
compareTo
を使用します。比較結果によって、数値が返却されるので、その値を元に処理してください。
assertThat(
BigDecimal.TEN.compareTo(BigDecimal.ONE)
).isEqualTo(1);
assertThat(
BigDecimal.ONE.compareTo(BigDecimal.TEN)
).isEqualTo(-1);
assertThat(
BigDecimal.ONE.setScale(1)
.compareTo(BigDecimal.ONE.setScale(2))
).isEqualTo(0);
equals
は精度が異なると一致しないので、精度に確信が無い限りは使わないでください。
assertThat(
BigDecimal.ONE.setScale(1)
.equals(BigDecimal.ONE.setScale(2))
).isEqualTo(false);
0除算を回避するために、事前に0ではないことを確認することもあります。その場合、BigDecimal.ZEROとのcompareTo
メソッドでも問題ないです。もっと処理速度を意識する時は、compareTo
メソッド内で呼んでいるsignum
メソッドを直接使った方が早いようです。
※ 私の手元環境で100万回実行した時の速度そんなに変わらなかったので、使いたい方を使ってください。
signum
メソッドは、値がプラスかマイナスか0かで判断します。
assertThat(
BigDecimal.ZERO
.compareTo(BigDecimal.ZERO.setScale(2))
).isEqualTo(0);
assertThat(
BigDecimal.ZERO.signum()
).isEqualTo(0);
±変換
negate
メソッドを使用します。単純に-1を掛け算する必要が無いので、メソッドが見やすいです。
assertThat(
BigDecimal.ONE.negate()
).isEqualTo(BigDecimal.valueOf(-1));
assertThat(
BigDecimal.ONE.negate().negate()
).isEqualTo(BigDecimal.valueOf(1));
文字列化
toPlainString
メソッドを使用します。toString
メソッドだと、有効桁数や精度に合わせて丸めた表記になってしまうので、意図しないものが表示される可能性があります。意図している場合は、toString
を使用しましょう。
assertThat(
BigDecimal.valueOf(0.0000001).toString()
).isEqualTo("1.0E-7");
assertThat(
BigDecimal.valueOf(0.0000001).toPlainString()
).isEqualTo("0.00000010");
リストのBigDecimalを計算する
BigDecimalはプリミティブ型ではないので、IntStream
やLongStream
等の便利な型はありません。
StreamAPIのreduce
メソッドを使用すると、目的通りの計算をすることができます。
次の例は1を10個含むリストから計算する方法です。
List<BigDecimal> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(BigDecimal.ONE);
}
assertThat(
list.stream().reduce(
BigDecimal.ZERO, BigDecimal::add
)
).isEqualTo(BigDecimal.TEN);
assertThat(
list.stream().reduce(
BigDecimal.TEN, BigDecimal::subtract
)
).isEqualTo(BigDecimal.ZERO);
RoundingModeの一覧
非常に具体的な例がJavaDocにも記載されているので、こちらを参考にしてください。具体的な値と丸め方が表形式になっているので、こちらに関しては公式を見るのが確実です。
docs.oracle.com
1.00000を1として扱う
stripTrailingZeros
を実行すると、1.000等々の0で埋めた実際の小数点を確認できます。当然、小数点3桁目に0以外が含まれていた場合は、3桁の小数点であるという精度を表示してくれます。
assertThat(
new BigDecimal("1.00000000000").scale()
).isEqualTo(11);
assertThat(
new BigDecimal("1.00000000000").stripTrailingZeros().scale()
).isEqualTo(0);
assertThat(
new BigDecimal("1.00000000001").scale()
).isEqualTo(11);
assertThat(
new BigDecimal("1.00000000001").stripTrailingZeros().scale()
).isEqualTo(11);
ソースコード
BigDecimalの検証用ソース
github.com
終わりに
なんだかんだ、今まではお金を直接扱うことが無かったのでBigDecimalを使う機会がありませんでした。
普段使わないので色々調べていくと、自分が理解していなかったことも多かったです。有効桁数と精度に関しては本当に分かってませんでした。
公式のJavaDoc見ればいいのでそんなに調べる機会は無いと思いますが、腰を据えて調べたくなった時に今後も記事にしていきたいです。
参考資料
Java16(en)
docs.oracle.com
Java8(ja)
docs.oracle.com
Java8のRoundingMode
docs.oracle.com
【Java】BigDecimalをちゃんと使う~2018~
qiita.com
BigDecimal, precision and scale
stackoverflow.com