自分が知りたかったBigDecimalの知識を集める記事。なお、基本型を扱う関係上、新規性は多くありません。強いて言えば、2021年でも扱えるってことくらいですかね。
環境
- Java
- 15
この記事を読んでわかること一覧
- precisionとscale(有効桁数と精度)
- 型宣言
- 足し算
- 引き算
- 掛け算
- 割り算
- 10倍や100倍、または1/10や1/100
- 余り
- 四捨五入等の丸めを行う
- 比較
- ±変換
- 文字列化
- リストのBigDecimalを計算する
- 1.00000を1に丸める
precisionとscale(有効桁数と精度)
BigDecimalを扱うのであれば抑えておくべき内容。
- precision
- 有効桁数。最初に0以外の数字が出てからの桁数。
- scale
- 精度。小数点以下の桁数。
scaleはsetScale
メソッドが存在するが、precisionに関してはコンストラクタか、掛け算や割り算のパラメータMathContext
で設定しなければならない。
(例)
BigDecimal example = BigDecimal.valueOf(100.00001); example.scale(); // 5 example.precision(): // 8 BigDecimal example2 = BigDecimal.valueOf(0.00001); example2.scale(); // 5(実際には0.000010が生成されて6になる) example.precision(); // 1(実際には0.000010が生成されて2になる)
型宣言
BigDecimalの型宣言をする方法。
valueOf
のstaticファクトリーメソッドを使いましょう。new 時にDouble型を渡すと、意図しない数値になります。
System.out.println(new BigDecimal(123.45)); // 123.4500000000000028421709430404007434844970703125 System.out.println(new BigDecimal("123.45")); // 123.45 System.out.println(BigDecimal.valueOf(123.45)); // 123.45
また、0, 1, 10に関してはstatic変数が用意されているので、こちらを使用しましょう。
BigDecimal.ONE; BigDecimal.ZERO; BigDecimal.TEN;
足し算
add
メソッドを使用します。また、小数点以下の桁数は多い方に合わせます。
(例)1.0 + 1.00 = 2.00
BigDecimal.ONE.add(BigDecimal.ONE);
// BigDecimal.valueOf(2);
引き算
subtract
メソッドを使用します。また、小数点以下の桁数は多い方に合わせます。
(例)1.0 - 1.00 = 0.00
BigDecimal.ONE.subtract(BigDecimal.ONE);
// BigDecimal.valueOf(0);
掛け算
multiply
メソッドを使用します。
単純に掛け算するタイプのメソッドと、有効桁数を設定して丸め設定をするタイプのメソッドがあります。計算する桁数が膨大なのであまり細かく計算する必要が無い場合は、後者を使用しましょう。
// 12345.6789 * 10 で有効桁数を2桁、それ以下を切り捨てする計算 assertThat( BigDecimal.valueOf(12345.6789) .multiply(BigDecimal.TEN, new MathContext(2, RoundingMode.DOWN)) .toPlainString() ).isEqualTo("120000"); // 12345.6789 * 10 でscaleが変わっていないため、計算結果が123456.7890となる。 assertThat( BigDecimal.valueOf(12345.6789) .multiply(BigDecimal.TEN) .toPlainString() ).isEqualTo("123456.7890");
割り算
divide
メソッドを使用します。
単純に割り算するタイプのメソッドと、有効桁数を設定して丸め設定をするタイプのメソッド、精度を設定して丸め設定をするタイプのメソッドがあります。
割り切れないとExceptionが発生するので、必ずRoundingMode
は使用しましょう。
// 単純に1 / 3 の割り算する. 割り切れないとArithmeticExceptionが発生する. assertThatThrownBy( () -> BigDecimal.ONE.divide(BigDecimal.valueOf(3)) ).isInstanceOfSatisfying( ArithmeticException.class, (e) -> e.getMessage().equals("Non-terminating decimal expansion; no exact representable decimal result.") ); // 1 / 3 の割り算をした後に、第二引数のscaleで丸める(切り上げする) assertThat( BigDecimal.ONE .divide(BigDecimal.valueOf(3), 3, RoundingMode.UP) .toPlainString() ).isEqualTo("0.334"); // 1 / 3 の割り算をした後に、MathContextの有効数字で丸める(切り上げする) assertThat( BigDecimal.ONE .divide(BigDecimal.valueOf(3), new MathContext(3, RoundingMode.UP)) .toPlainString() ).isEqualTo("0.334");
また、エラーにならないように適宜丸め処理を行う関係上、精度は意識しておきましょう。
精度を増やさないままに丸めてしまうと、意図しない結果になりかねません。
// 1 / 3の精度を増やさなかったので、0.3を切り上げて1になってしまう assertThat( BigDecimal.ONE .divide(BigDecimal.valueOf(3), RoundingMode.UP) .toPlainString() ).isEqualTo("1"); // 1 / 3 の割り算をする前に精度を増やしておき、増やした精度で丸める(切り上げする) assertThat( BigDecimal.ONE .setScale(3, RoundingMode.UP) .divide(BigDecimal.valueOf(3), RoundingMode.UP) .toPlainString() ).isEqualTo("0.334"); // (1 / 3) * 100で割り算で切り上げてしまったので、小数点以下が変になる assertThat( BigDecimal.ONE .divide(BigDecimal.valueOf(3), 3, RoundingMode.UP) .multiply(BigDecimal.valueOf(100)) .toPlainString() ).isEqualTo("33.400"); // (1 / 3) * 100で割り算の精度を十分用意しておき、後で丸めると目的通りになる 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倍が簡単にできます。
// 10倍 BigDecimal.ONE.scaleByPowerOfTen(1); // 100倍 BigDecimal.ONE.scaleByPowerOfTen(2); // 1/10倍 BigDecimal.ONE.scaleByPowerOfTen(-1); // 1/100倍 BigDecimal.ONE.scaleByPowerOfTen(-2);
movePointRight
メソッドで10倍やmovePointLeft
メソッドで1/10倍も簡単にできます。
// 10倍 BigDecimal.TEN.movePointRight(1); // 1/10倍 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"); // 1.0が期待値?1ではない? 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
をすると、意図を表現しやすいです。処理順序に左右される意図的な丸めを発生させない時に使用します。
// (1/3) * 100を最後に3桁の切り上げで丸める。 assertThat( BigDecimal.ONE // 精度1桁 .setScale(6, RoundingMode.UP) // 精度6桁 .divide(BigDecimal.valueOf(3), RoundingMode.UP) // 精度6桁 .multiply(BigDecimal.valueOf(100)) // 精度6桁 .setScale(3, RoundingMode.UP) // 精度3桁 .toPlainString() ).isEqualTo("33.334");
比較
compareTo
を使用します。比較結果によって、数値が返却されるので、その値を元に処理してください。
- A > B
- 1
- A < B
- -1
- A = B
- 0
// 10 > 1の時は1 assertThat( BigDecimal.TEN.compareTo(BigDecimal.ONE) ).isEqualTo(1); // 1 < 10の時は-1 assertThat( BigDecimal.ONE.compareTo(BigDecimal.TEN) ).isEqualTo(-1); // 一致している時は0 // 0.0 と 0.00の比較 assertThat( BigDecimal.ONE.setScale(1) .compareTo(BigDecimal.ONE.setScale(2)) ).isEqualTo(0);
equals
は精度が異なると一致しないので、精度に確信が無い限りは使わないでください。
// 0.0と0.00の比較はfalseになる assertThat( BigDecimal.ONE.setScale(1) .equals(BigDecimal.ONE.setScale(2)) ).isEqualTo(false);
0除算を回避するために、事前に0ではないことを確認することもあります。その場合、BigDecimal.ZEROとのcompareTo
メソッドでも問題ないです。もっと処理速度を意識する時は、compareTo
メソッド内で呼んでいるsignum
メソッドを直接使った方が早いようです。
※ 私の手元環境で100万回実行した時の速度そんなに変わらなかったので、使いたい方を使ってください。
signum
メソッドは、値がプラスかマイナスか0かで判断します。
// 0 と比較 assertThat( BigDecimal.ZERO .compareTo(BigDecimal.ZERO.setScale(2)) ).isEqualTo(0); // 絶対値がプラスでもマイナスでもない assertThat( BigDecimal.ZERO.signum() ).isEqualTo(0);
±変換
negate
メソッドを使用します。単純に-1を掛け算する必要が無いので、メソッドが見やすいです。
// 1 * -1 = -1 assertThat( BigDecimal.ONE.negate() ).isEqualTo(BigDecimal.valueOf(-1)); // 1 * -1 * -1 = 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); } // 0 + 1 * 10の計算 assertThat( list.stream().reduce( BigDecimal.ZERO, BigDecimal::add ) ).isEqualTo(BigDecimal.TEN); // 10 - 1 * 10の計算 assertThat( list.stream().reduce( BigDecimal.TEN, BigDecimal::subtract ) ).isEqualTo(BigDecimal.ZERO);
RoundingModeの一覧
非常に具体的な例がJavaDocにも記載されているので、こちらを参考にしてください。具体的な値と丸め方が表形式になっているので、こちらに関しては公式を見るのが確実です。
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