BigDecimal.divideでArithmeticExceptionになる原因と丸め方を解説する記事のアイキャッチ。

Java クイズ

BigDecimal.divideでArithmeticExceptionになる原因|割り切れない計算の丸め方

AIによる要約

BigDecimalのdivideは、結果を正確に有限小数で表せない場合、丸め指定なしではArithmeticExceptionを投げます。1÷3のような計算では、業務仕様に基づいてscaleとRoundingModeを明示します。例外を避けるため適当に丸めるのではなく、金額、税率、表示値、DB保存値のどの段階で何桁にするかを決めることが重要です。

この記事はXへ投稿したJavaクイズの詳しい解説です。まず問題を解きたい方は、Xの元ポストを開くことができます。

新人SE
新人SE
BigDecimalなら高精度なのに、1を3で割るだけで例外になるんですか?
ポンコツSE
ポンコツSE
正確な結果を有限小数で表せないためです。Javaが勝手に丸めず、呼び出し側へ判断を求めています。

金額、単価、税率、進捗率などを扱う業務システムでは、BigDecimalをよく使います。doubleより安全という説明だけを覚え、divideも普通の割り算と同じ感覚で呼ぶと、本番データで突然ArithmeticExceptionになることがあります。

開発環境では100÷4のような割り切れる値しか試しておらず、本番で1÷3や10÷6が来て初めて落ちるのが典型です。対策は例外をcatchすることではなく、必要な桁数と丸め方法を業務仕様として決め、コードとテストへ残すことです。

この記事のポイント

  • 割り切れないdivideは丸め指定なしで例外になる
  • scaleは小数点以下の桁数を表す
  • RoundingModeは業務仕様から選ぶ
  • 計算途中と最終表示の丸めを混同しない
  • 0除算とnullも別に確認する

まずはXのJavaクイズを確認する

BigDecimalの1を3でdivideした結果を問うJavaクイズ画像。
0.333...、0、ArithmeticExceptionのどれになるでしょうか。
BigDecimal one = new BigDecimal("1");
BigDecimal three = new BigDecimal("3");

System.out.println(one.divide(three));

答えはArithmeticExceptionです。1÷3は有限小数で正確に表せないため、丸め方法を指定しないdivideは処理を続けられません。

BigDecimalで1を3で割るとArithmeticExceptionになり、丸め指定が必要と説明する画像。
答えはArithmeticExceptionです。Javaは丸め方を勝手に決めません。

元の投稿は、Xの出題ポスト回答ポストでも確認できます。

divideが例外を投げる条件

divide(BigDecimal divisor)は、商を正確に表せる場合だけ結果を返します。2÷4は0.5、1÷8は0.125なので有限小数です。一方、1÷3は3が無限に続きます。BigDecimalは無限桁を保持できないため、丸め指定がなければ例外にします。

System.out.println(new BigDecimal("1").divide(new BigDecimal("8")));
// 0.125

System.out.println(new BigDecimal("1").divide(new BigDecimal("3")));
// ArithmeticException: Non-terminating decimal expansion

この挙動は不親切なのではなく、金額を勝手に切り捨てたり四捨五入したりしないための安全策です。どの丸めが正しいかは、Javaではなく業務ルールが決めます。

scaleとRoundingModeを指定する

小数点以下2桁へ丸めるなら、scaleへ2、丸め方法へRoundingModeを渡します。HALF_UPは一般的な四捨五入に近い動作ですが、案件の会計ルールが常にHALF_UPとは限りません。

BigDecimal result = one.divide(three, 2, RoundingMode.HALF_UP);
System.out.println(result); // 0.33

DOWNは0方向への切り捨て、UPは0から離れる方向への切り上げです。負数を扱う場合、「切り捨て」という日本語だけでは方向が曖昧になります。仕様書にはRoundingMode相当の動作例まで残します。

金額計算で丸める場所を決める

同じscaleとRoundingModeでも、明細ごとに丸めてから合計するか、合計後に一度だけ丸めるかで結果が変わります。請求、税、ポイント計算では1円差が監査や問い合わせにつながります。

BigDecimal unitPrice = new BigDecimal("100");
BigDecimal taxRate = new BigDecimal("0.08");

BigDecimal tax = unitPrice.multiply(taxRate)
        .setScale(0, RoundingMode.DOWN);

このコードだけを見ても、明細税を切り捨てる仕様かは判断できません。税額計算の単位、端数処理のタイミング、保存するscale、画面表示の桁数を確認し、命名や専用メソッドで意図を表します。

MathContextとの使い分け

MathContextは小数点以下の桁数ではなく、有効桁数と丸め方法を指定します。科学計算や複数演算で精度方針を統一する用途がありますが、金額の小数点以下2桁を保証したい場面ではscale指定の方が意図を伝えやすいことがあります。

MathContext context = new MathContext(4, RoundingMode.HALF_UP);
BigDecimal result = new BigDecimal("1234")
        .divide(new BigDecimal("7"), context);

System.out.println(result); // 176.3

新人のうちは、既存コードがMathContextを使っているからという理由だけで混在させないでください。プロジェクトの数値計算ルールや共通ユーティリティを確認し、同じ意味の値に異なる精度方針を持ち込まないことが大切です。

0除算と入力値の検証

除数が0の場合は、丸め方法を指定してもArithmeticExceptionです。割合を計算する処理では、合計件数0件、稼働時間0、基準金額0などが実際に起こり得ます。

BigDecimal rate(BigDecimal completed, BigDecimal total) {
    if (total.signum() == 0) {
        return BigDecimal.ZERO; // 0件時の仕様を確認して決める
    }
    return completed.divide(total, 4, RoundingMode.HALF_UP);
}

0件なら0%とするのか、計算不能として空欄にするのか、異常として例外にするのかは業務次第です。とりあえずZEROを返すと、未集計と達成率0%を区別できなくなるため、呼び出し元の表示まで確認します。

計算ルールを1か所へ集める

各Serviceが個別にsetScaleを書くと、HALF_UPとDOWNが混在しやすくなります。同じ業務値なら、値オブジェクトや計算サービスへルールを集めます。

final class TaxCalculator {
    private static final int MONEY_SCALE = 0;
    private static final RoundingMode TAX_ROUNDING = RoundingMode.DOWN;

    BigDecimal calculate(BigDecimal amount, BigDecimal rate) {
        return amount.multiply(rate)
                .setScale(MONEY_SCALE, TAX_ROUNDING);
    }
}

定数化するだけで仕様が正しくなるわけではありませんが、変更箇所とレビュー対象を絞れます。名前に税額用であることを含め、別用途の割合計算へ安易に流用しないようにします。

JUnitで境界値と丸め方向を固定する

テストでは割り切れる値だけでなく、正の端数、負の端数、ちょうど半分、0、非常に大きい値を確認します。期待値もBigDecimalで表し、必要ならcompareToで数値として比較します。

@Test
void 端数を小数2桁で四捨五入する() {
    BigDecimal actual = new BigDecimal("1")
            .divide(new BigDecimal("6"), 2, RoundingMode.HALF_UP);

    assertEquals(0, new BigDecimal("0.17").compareTo(actual));
}

@Test
void 除数0は業務例外に変換する() {
    assertThrows(IllegalArgumentException.class,
            () -> calculator.rate(BigDecimal.ONE, BigDecimal.ZERO));
}

例外型を変換する場合は、呼び出し側がどう扱うかも合わせてテストします。ArithmeticExceptionを広いcatchで握り潰し、0を返すだけの修正は避けます。

現場レビューでよくある指摘

BigDecimalのレビューでは、例外が出ないことより、桁数と丸め方法が業務仕様に一致しているかを確認します。

// レビューコメント例
1÷3のような値でArithmeticExceptionになります。
この率は小数何桁で、どの丸め方法にする仕様か確認してください。

// レビューコメント例
明細単位で丸めるか、合計後に丸めるかで金額が変わります。
税額計算書の端数処理単位をテストへ反映してください。

// レビューコメント例
除数0件を0%としてよいか判断できません。
未集計と0%を区別する画面仕様まで確認してください。

『例外が出たのでHALF_UPを足しました』では根拠が足りません。仕様書、既存の共通計算、DBカラム、画面表示を確認し、同じルールが一貫していることを説明します。

提出前のセルフチェック

レビュー前に確認すること

  • 割り切れない値をテストしたか
  • scaleの根拠を説明できるか
  • RoundingModeが業務仕様に一致するか
  • 明細単位か合計単位か決めたか
  • 負数の丸め方向を確認したか
  • 除数0の扱いを決めたか
  • DB保存値と画面表示の桁数を確認したか
  • 広いcatchでArithmeticExceptionを隠していないか

Javaの数値型とAPIを整理する参考書

BigDecimalはコンストラクタ、比較、演算、丸めが互いに関係します。基本APIをまとまった形で確認すると、金額処理を場当たり的に直すのを避けられます。

書籍「スッキリわかるJava入門 実践編 第5版」の表紙
商品画像:Amazon.co.jp
PR Java基礎学習後の若手向け

スッキリわかるJava入門 実践編 第5版

中山 清喬 (著), 株式会社フレアリンク (監修)

基礎文法の次に必要な、現場寄りのJava知識を補う。

コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。

  • Java基礎の次に何を学ぶか迷っている
  • コレクションやStreamを整理したい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

この記事とあわせて読みたい

まとめ

BigDecimalのdivideは、商を正確な有限小数で表せない場合、丸め指定なしではArithmeticExceptionになります。scaleとRoundingModeを指定すれば処理できますが、その値は業務仕様から決めます。

金額や率では、丸める桁だけでなく、明細単位か合計単位か、0件をどう扱うかまで結果へ影響します。境界値をテストし、計算ルールを1か所へ集めてください。

-Java, クイズ
-, , ,