テックブログ

Java BigDecimal入門:金額計算でdoubleを使わない理由

Java BigDecimal入門:金額計算でdoubleを使わない理由

JavaのBigDecimalは、金額や税率など正確な小数計算が必要な場面で使う数値型です。doublefloatは2進数で小数を表すため、0.1のような値でも内部的に誤差が発生することがあります。BigDecimalを使うと、計算精度、丸め方法、小数点以下の桁数を明示しながら扱えるため、請求、決済、会計処理などの金額計算で安全に設計しやすくなります。

1. BigDecimalとは何か

1-1. 正確な小数計算を扱うための型

BigDecimalは、誤差を避けたい小数計算を扱うためのJavaの数値型です。特に金額、税率、割引率、単価など、計算結果のズレが問題になる場面で使われます。

intlongは整数を扱う型で、doublefloatは小数を扱える型です。ただし、doublefloatは内部的に2進数で近似値として小数を表すため、10進数で見るとわずかな誤差が出ることがあります。BigDecimalは10進数の桁数や丸めを明示しやすく、金額のように正確性が求められる値に向いています。

たとえば、請求金額が999.99円なのか1000.00円なのかは、業務上大きな違いです。小数を扱うからといって何でもBigDecimalにする必要はありませんが、金額や会計に関わる計算では最初からBigDecimalを選ぶのが基本です。

1-2. 金額計算で使われる理由

金額計算でBigDecimalが使われる理由は、計算の正確性と丸めルールを明示できるからです。請求、決済、給与、税計算では、わずかな誤差でもトラブルにつながります。

金額計算では、税率を掛ける、割引率を掛ける、合計金額を出す、端数を丸めるといった処理がよくあります。このとき、どの桁で丸めるのか、切り上げるのか、切り捨てるのか、四捨五入するのかを明確にする必要があります。BigDecimalは、RoundingModescaleを使って、こうしたルールをコード上に表現できます。

たとえば、消費税計算で「小数点以下を切り捨てる」のか「四捨五入する」のかは、システムや契約条件によって変わります。BigDecimalを使うだけで正解になるのではなく、業務ルールに合った丸め方法を指定することが重要です。

1-3. double/floatとの違い

BigDecimaldouble/floatの大きな違いは、小数を正確に扱う目的か、近似値として高速に扱う目的かです。用途によって向き不向きがあります。

doublefloatは、科学技術計算、座標計算、グラフィック処理など、多少の誤差を許容しながら高速に計算したい場面でよく使われます。一方、BigDecimalは、金額や税率のように10進数としての正確性が必要な場面に向いています。ただし、BigDecimaldoubleより扱いが冗長で、計算にも専用メソッドを使います。

つまり、doublefloatが常に悪いわけではありません。座標や統計値などでは自然な選択になることもあります。しかし、「1円単位で正確に扱う」「税額を決める」「請求金額を保存する」といった処理では、BigDecimalを使うべき場面が多いです。

2. doubleで小数誤差が起きる理由

2-1. 2進数で表せない小数がある

doubleで小数誤差が起きる理由は、10進数では簡単に表せる小数でも、2進数では正確に表せないことがあるからです。これはJavaだけでなく、多くのプログラミング言語で共通する性質です。

コンピュータ内部では、多くの数値が2進数で表現されます。10進数の0.5は2進数でもきれいに表せますが、0.10.2は2進数では有限桁で表せません。そのため、doubleでは非常に近い値に丸めて保持されます。この近似値が、計算を重ねると見える形の誤差になります。

これはdoubleのバグではなく、浮動小数点数の仕組みによるものです。誤差を許容できる計算では問題にならないこともありますが、金額計算では「少しズレてもよい」とは言えないため注意が必要です。

2-2. 0.1 + 0.2 のような誤差の例

doubleでは、0.1 + 0.2の結果が期待する0.3ぴったりにならないことがあります。初心者が小数誤差を理解するうえで代表的な例です。

0.10.2は、double内部では正確な10進数としてではなく、近似値として保持されます。その近似値同士を足すため、表示すると0.30000000000000004のような結果になることがあります。このような誤差は、比較や丸めを考えずに使うと不具合の原因になります。

double a = 0.1;
double b = 0.2;

System.out.println(a + b);
// 0.30000000000000004 のように表示されることがある

このコードは、doubleが10進数の小数を常に正確に表すわけではないことを示しています。注意点は、画面表示では丸められて問題が見えない場合もあることです。内部値を使って金額判定や一致比較をすると、思わぬズレが表面化します。

2-3. 誤差が金額計算で問題になる理由

小数誤差が金額計算で問題になるのは、計算結果が請求額、支払額、税額としてそのまま業務上の意味を持つからです。わずかな差でも無視できない場面があります。

たとえば、単価、数量、税率、割引率を組み合わせると、1回の計算では小さな誤差でも、集計時にズレが積み重なることがあります。さらに、金額では最終的に円単位や小数第2位などに丸める必要があり、丸めのタイミングや方法も結果に影響します。doubleの近似誤差と業務上の丸めルールが混ざると、原因を追いにくいバグになります。

たとえば、ECサイトで商品ごとの税込金額を出すのか、合計金額に税率を掛けるのかでも結果が変わることがあります。金額計算では、型の選択だけでなく、丸める単位、丸めるタイミング、保存する桁数まで設計する必要があります。

3. BigDecimalの基本的な使い方

3-1. BigDecimal.valueOf() と文字列生成

BigDecimalを作るときは、BigDecimal.valueOf()または文字列を使った生成が基本です。特に小数を直接扱う場合は、生成方法で精度が変わることに注意します。

BigDecimal.valueOf(0.1)は、double値をそのまま不正確に展開するのではなく、文字列表現を経由して扱いやすい値にします。また、new BigDecimal("0.1")のように文字列から生成すると、10進数として意図した値を明確に表現できます。金額や税率の定数では、文字列生成がよく使われます。

BigDecimal price = BigDecimal.valueOf(1200);
BigDecimal taxRate = new BigDecimal("0.10");

BigDecimal tax = price.multiply(taxRate);

System.out.println(tax); // 120.00 ではなく 120.00 になるとは限らず、scaleに依存する

このコードでは、金額と税率をBigDecimalで扱っています。注意点は、BigDecimalには値だけでなくscale、つまり小数点以下の桁数の情報もあることです。表示上の桁数を整えたい場合は、計算とは別に表示用の整形も考える必要があります。

3-2. add / subtract / multiply / divide の基本

BigDecimalの加減乗除では、+-演算子ではなく、専用メソッドを使いますaddsubtractmultiplydivideが基本です。

BigDecimalはオブジェクトなので、プリミティブ型のようにa + bとは書けません。また、BigDecimalは基本的に不変オブジェクトです。計算メソッドを呼び出しても元の値が変更されるのではなく、新しいBigDecimalが返ります。そのため、結果を変数に受け取る必要があります。

BigDecimal price = new BigDecimal("1000");
BigDecimal tax = new BigDecimal("100");

BigDecimal total = price.add(tax);
BigDecimal discounted = total.subtract(new BigDecimal("50"));
BigDecimal doubled = discounted.multiply(new BigDecimal("2"));

System.out.println(total);      // 1100
System.out.println(discounted); // 1050
System.out.println(doubled);    // 2100

このコードでは、加算、減算、乗算をBigDecimalのメソッドで行っています。注意点は、price.add(tax);だけを書いてもprice自体は変わらないことです。計算結果は必ず新しい変数に代入するか、戻り値として使いましょう。

3-3. new BigDecimal(double) を避ける理由

new BigDecimal(double)は、小数の誤差をそのままBigDecimalに持ち込む可能性があるため避けるべきです。これはBigDecimal初心者が特にハマりやすい悪い例です。

new BigDecimal(0.1)と書くと、見た目は正確な0.1を作っているように見えます。しかし、引数の0.1は先にdoubleとして扱われるため、すでに近似値になっています。その近似値をBigDecimalに変換すると、意図しない長い小数になることがあります。

// 悪い例
BigDecimal bad = new BigDecimal(0.1);

// 良い例
BigDecimal good1 = new BigDecimal("0.1");
BigDecimal good2 = BigDecimal.valueOf(0.1);

System.out.println(bad);
System.out.println(good1);
System.out.println(good2);

このコードでは、new BigDecimal(0.1)が避けたい例で、文字列生成またはBigDecimal.valueOf()が良い例です。注意点は、整数値ならBigDecimal.valueOf(1000)のように扱いやすいことです。小数を扱うときは、特に生成方法を確認しましょう。

4. 丸め処理とscale

4-1. divideで丸め指定が必要になる場面

BigDecimaldivideでは、割り切れない計算に対して丸め指定が必要になることがあります。指定しないと例外が発生する場合があります。

たとえば、1 ÷ 3は10進数では0.3333...と無限に続くため、有限桁のBigDecimalとしてそのまま表せません。このような場合、何桁まで残すのか、どのように丸めるのかを指定する必要があります。これは面倒に見えますが、金額計算ではむしろ重要な設計ポイントです。

BigDecimal total = new BigDecimal("100");
BigDecimal count = new BigDecimal("3");

BigDecimal average = total.divide(count, 2, RoundingMode.HALF_UP);

System.out.println(average); // 33.33

このコードでは、割り算の結果を小数第2位まで残し、四捨五入に近いHALF_UPで丸めています。注意点は、divideで丸め指定を省くと、割り切れない場合にArithmeticExceptionが発生することです。割り算ではscaleと丸めを意識しましょう。

4-2. RoundingModeの基本

RoundingModeは、BigDecimalの端数をどのように丸めるかを指定するための列挙型です。金額計算では、丸め方法を曖昧にしないことが重要です。

代表的なものに、切り上げのCEILING、切り捨て方向のDOWN、四捨五入に近いHALF_UP、銀行丸めとも呼ばれることがあるHALF_EVENなどがあります。どれを使うべきかは、技術だけでなく業務ルールで決まります。税額、手数料、割引額などは、仕様書や契約条件で丸め方法を確認する必要があります。

BigDecimal value = new BigDecimal("10.555");

System.out.println(value.setScale(2, RoundingMode.HALF_UP));   // 10.56
System.out.println(value.setScale(2, RoundingMode.DOWN));      // 10.55
System.out.println(value.setScale(2, RoundingMode.CEILING));   // 10.56

このコードでは、同じ値でも丸め方法によって結果が変わることを示しています。注意点は、「四捨五入」と書かれていても、業務上どの丸めを指すのか確認が必要なことです。なんとなくHALF_UPを使うのではなく、仕様として決めましょう。

4-3. 小数点以下の桁数をどう決めるか

scaleは、BigDecimalが持つ小数点以下の桁数です。金額計算では、計算用の桁数と表示用の桁数を分けて考えることが大切です。

たとえば、日本円の請求金額は最終的に0桁、つまり整数円で扱うことが多いです。一方、税率や中間計算では小数点以下の桁が必要になることがあります。途中で毎回丸めるのか、最後にまとめて丸めるのかによって結果が変わるため、scaleは単なる表示の問題ではありません。

BigDecimal amount = new BigDecimal("1234.567");

BigDecimal roundedForCalculation = amount.setScale(2, RoundingMode.HALF_UP);
BigDecimal roundedForYen = amount.setScale(0, RoundingMode.DOWN);

System.out.println(roundedForCalculation); // 1234.57
System.out.println(roundedForYen);         // 1234

このコードでは、小数第2位まで残す場合と、円単位で切り捨てる場合を分けています。注意点は、表示のためだけに丸めた値を、そのまま次の計算に使うと誤差やズレの原因になることです。計算値と表示値は目的を分けて扱いましょう。

5. よくある落とし穴

5-1. equalsとcompareToの違い

BigDecimalの比較では、equalscompareToの違いに注意が必要です。数値として同じに見えても、equalsではfalseになることがあります。

BigDecimal.equalsは、数値の大きさだけでなくscaleも比較します。一方、compareToは数値としての大小を比較します。そのため、new BigDecimal("1.0")new BigDecimal("1.00")は、数値としては同じですが、equalsでは異なると判定されます。

BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("1.00");

System.out.println(a.equals(b));           // false
System.out.println(a.compareTo(b) == 0);   // true

このコードでは、equalscompareToで結果が変わることを示しています。注意点は、金額の大小や同額判定ではcompareToを使う場面が多いことです。equalsを使うと、scale違いで意図しない不一致になることがあります。

5-2. scale違いで比較結果が変わる

BigDecimalでは、同じ数値でもscaleが違うと扱いが変わることがあります。特にequals、Mapのキー、Setの要素として使う場合に注意が必要です。

1.01.00は、人間から見ると同じ1ですが、BigDecimalではscaleが異なります。1.0のscaleは1、1.00のscaleは2です。equalsはこの違いも見るため、同じ金額のつもりでも別物として扱われることがあります。

たとえば、HashSet<BigDecimal>1.01.00を入れると、期待と違う結果になる可能性があります。金額として同額かを判定したいならcompareTo、表示桁や保存形式まで含めて同じかを見たいならequals、というように使い分けましょう。

5-3. 金額表示と計算値を混同する

金額計算では、計算に使う値と画面に表示する値を混同しないことが重要です。表示のための整形と、計算上の丸めは目的が違います。

画面では1,234円1,234.50のように見せたいことがありますが、これは表示形式の問題です。一方、計算値は税率や割引率を掛けるために、より細かい桁数を保持することがあります。表示用に丸めた値を次の計算に使うと、集計結果がズレる原因になります。

たとえば、明細ごとに小数点以下を丸めてから合計するのか、合計してから最後に丸めるのかで結果が変わることがあります。金額システムでは、表示の都合ではなく、業務ルールに従って丸めるタイミングを決めましょう。

6. まとめ

6-1. BigDecimalを使うべき場面

BigDecimalは、金額、税率、請求、決済、会計など、正確な10進小数計算が必要な場面で使うべき型です。小数を扱うすべての処理で必須というわけではありません。

doublefloatは、近似値で高速に計算したい場面に向いています。一方、金額計算では、わずかな誤差や丸めの違いが業務上の問題になります。そのため、BigDecimalを使い、生成方法、計算メソッド、scale、RoundingModeを明示することが大切です。

特に、new BigDecimal(double)を避けること、割り算では丸め指定をすること、金額比較ではequalscompareToの違いを理解することは、実務でよく効くポイントです。

6-2. 金額計算のチェックリスト

Javaで金額計算を実装するときは、型、生成方法、丸め、比較、表示を分けて確認すると失敗を減らせます。

  • 金額や税率にdouble/floatを使っていないか
  • BigDecimal.valueOf()または文字列から生成しているか
  • new BigDecimal(0.1)のような生成をしていないか
  • 加減乗除にaddsubtractmultiplydivideを使っているか
  • divideで割り切れない場合のscaleとRoundingModeを指定しているか
  • 丸め方法が業務ルールとして決まっているか
  • 同額判定にequalsではなくcompareToを使うべき場面を理解しているか
  • 計算値と表示用の整形を混同していないか

BigDecimalは、使えば自動的に金額計算が安全になる魔法の型ではありません。正しい生成方法と丸めルール、比較方法をセットで理解することで、請求や決済に耐えやすい実装になります。

7. 参考リンク