きり丸の技術日記

技術・エンジニアのイベント・資格等はこちらにまとめる予定です

JavaのBigDecimalをまとめる(リストのBigDecimalの計算も)【2021年】

自分が知りたかったBigDecimalの知識を集める記事。なお、基本型を扱う関係上、新規性は多くありません。強いて言えば、2021年でも扱えるってことくらいですかね。

環境

この記事を読んでわかること一覧

  • precisionとscale(有効桁数と精度)
  • 型宣言
  • 足し算
  • 引き算
  • 掛け算
  • 割り算
  • 10倍や100倍、または1/10や1/100
  • 余り
  • 四捨五入等の丸めを行う
  • 比較
  • ±変換
  • 文字列化
  • リストのBigDecimalを計算する

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はプリミティブ型ではないので、IntStreamLongStream等の便利な型はありません。

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にも記載されているので、こちらを参考にしてください。具体的な値と丸め方が表形式になっているので、こちらに関しては公式を見るのが確実です。

docs.oracle.com

ソースコード

BigDecimalの検証用ソース github.com

終わりに

なんだかんだ、今まではお金を直接扱うことが無かったのでBigDecimalを使う機会がありませんでした。

普段使わないので色々調べていくと、自分が理解していなかったことも多かったです。有効桁数と精度に関しては本当に分かってませんでした。

公式のJavaDoc見ればいいのでそんなに調べる機会は無いと思いますが、腰を据えて調べたくなった時に今後も記事にしていきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

参考資料

Java16(en) docs.oracle.com

Java8(ja) docs.oracle.com

Java8のRoundingMode docs.oracle.com

JavaBigDecimalをちゃんと使う~2018~ qiita.com

BigDecimal, precision and scale stackoverflow.com

f:id:nainaistar:20210322191647p:plain