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

実装・エラー解決

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も別に確認する

割合の丸めと分母0をJUnitで確かめる

この記事のRateCalculatorとJUnitの全文、実行設定を含みます。JDK 17とMaven 3.9系で、展開先から mvn test を実行します。

まずは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の場合は、丸め方法を指定してもBigDecimal.divideはArithmeticExceptionを投げます。以下の設備件数の割合では、総件数0はまだ割合を出せない状態なので、割り算の前にIllegalArgumentExceptionを投げます。総件数が正で完了件数だけが0なら、計算した割合として0.0000を返します。

RateCalculator.java の全文です。

package example.bigdecimal;

import java.math.BigDecimal;
import java.math.RoundingMode;

public final class RateCalculator {
    public BigDecimal rate(BigDecimal completed, BigDecimal total) {
        if (total.signum() == 0) {
            throw new IllegalArgumentException("total must not be zero");
        }
        return completed.divide(total, 4, RoundingMode.HALF_UP);
    }
}

総件数0の表示は仕様に合わせます。たとえば画面に「対象なし」と出す場合は、画面側でその条件を扱います。この例のrateは比率を小数4桁で返すので、完了2件・総件数3件なら0.6667です。割合を計算できない状態と、計算した結果が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で数値として比較します。

RateCalculatorTest.java の全文です。

package example.bigdecimal;

import java.math.BigDecimal;
import java.math.RoundingMode;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

class RateCalculatorTest {
    private final RateCalculator calculator = new RateCalculator();

    @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 達成率を小数4桁で四捨五入する() {
        BigDecimal actual = calculator.rate(new BigDecimal("2"), new BigDecimal("3"));

        assertEquals(new BigDecimal("0.6667"), actual);
    }

    @Test
    void 総件数が正で完了件数が0なら達成率は0() {
        BigDecimal actual = calculator.rate(BigDecimal.ZERO, new BigDecimal("3"));

        assertEquals(new BigDecimal("0.0000"), actual);
    }

    @ParameterizedTest
    @ValueSource(ints = {0, 1})
    void 総件数が0なら未計算として例外にする(int completed) {
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> calculator.rate(BigDecimal.valueOf(completed), BigDecimal.ZERO));

        assertEquals("total must not be zero", exception.getMessage());
    }
}

RateCalculatorTestでは、総件数0の2通りで例外型とメッセージを、総件数3では比率と桁数を確認します。小数2桁の1 ÷ 6 = 0.17は丸めAPI単体の比較例です。rateの小数4桁とはテストを分けています。

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

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でご確認ください。

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

関連記事JavaのBigDecimalでequalsがfalseになる理由|0.0と0.00はcompareToで比較する? 0.0と0.00の比較、scaleとcompareToの違いを確認できます。 記事を読む 関連記事JavaでNumberFormatExceptionになる原因|空文字・カンマ・全角数字の対処 画面やCSVの文字列を数値へ変換する前段の注意点を解説します。 記事を読む 関連記事Javaのスタックトレースの読み方|Caused byの後、どのコードを開けばいい? 例外名だけでなく、causeと呼び出し位置を調査する方法を確認します。 記事を読む

まとめ

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

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

-実装・エラー解決
-,