2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Java数値計算の罠】BigDecimal の比較で equals() を使い「1.0」と「1.00」が不一致で計算事故になった話

2
Posted at

Javaで金額計算や精度の求められる数値処理を書く際、double や float の丸め誤差(浮動小数点数演算の誤差)を避けるために使われる BigDecimal。

「double の誤差事故を防ぐために BigDecimal を使っていれば、数値計算は完璧で安心!」

…そう信じ切っていた時期が私にもありました。

しかしある日、数値としてはまったく同じはずの数値判定で不一致が発生。特定の条件分岐を通り抜けてしまい、金額の割引計算が適用されないまま決済されてしまう計算事故を引き起こしました。

今回は、なぜ BigDecimal で「1.0」と「1.00」が等しいと判定されなかったのか、その原因と正しい比較方法についてまとめます。


事故の全貌:数値としては等しいはずなのに判定をスルーされた

状況

ECサイトの「購入金額に応じた手数料の無料化ロジック」を実装していた時のことです。
商品の「適用倍率(magnification)」という項目があり、これが 「1.0」(等倍=標準)の場合は特別手数料を免除する、という判定ロジックを作成しました。

コードのイメージは以下のような形でした。

public class FeeCalculator {

    // 標準の倍率(1.0)
    private static final BigDecimal DEFAULT_MAGNIFICATION = new BigDecimal("1.0");

    public BigDecimal calculateFee(OrderDto order) {
        BigDecimal magnification = order.getMagnification();

        // 倍率が 1.0 の場合は手数料無料
        if (DEFAULT_MAGNIFICATION.equals(magnification)) {
            return BigDecimal.ZERO;
        }

        // それ以外は通常の手数料計算
        return order.getAmount().multiply(magnification).multiply(new BigDecimal("0.05"));
    }
}

単体テストでは、new BigDecimal("1.0") をセットしてテストを実行し、問題なくパスしていました。

発生した悲劇

しかし、本番環境で特定の取引先から送信された注文データで、本来無料になるはずの手数料が加算されているという問合せが入りました。

同僚:「〇〇さん、倍率 1.0 で登録されている注文なのに、なぜか手数料が引き落とされています!」
私:「えっ、倍率が 1.0 なら equals() で判定して手数料 0 になるはずですが…!?」

ログを確認すると、入力された magnification には 1.00 という値が入っていました。
画面やデータベースの定義上はどちらも「数値の1」ですが、DEFAULT_MAGNIFICATION.equals(magnification) の評価結果が false になり、条件分岐をスルーして手数料が計算されてしまっていたのです。


考察:なぜ equals() で「1.0」と「1.00」が不一致になるのか?

原因を調査した結果、BigDecimal クラスにおける equals() の仕様を誤解していたことが判明しました。

BigDecimal.equals() は「精度(scale)」まで厳密に比較する

Javaの一般的なオブジェクトにおいて equals() は「同値性」を比較しますが、BigDecimal における equals() は、「数値(unscaled value)」と「精度(scale)」の両方が完全に一致している場合のみ true を返します。

BigDecimal a = new BigDecimal("1.0");   // unscaledValue: 10, scale: 1
BigDecimal b = new BigDecimal("1.00");  // unscaledValue: 100, scale: 2

System.out.println(a.equals(b)); // false !
  • a("1.0")の scale は 1
  • b("1.00")の scale は 2

数学的には 1.0 == 1.00 ですが、BigDecimal オブジェクトとしては 「保持している精度(小数桁数)が異なるため別物」 と判断されます。

Objects.equals() を使っても解決しない

「null 安全にするために Objects.equals(a, b) を使えば大丈夫」と思って書いても、内部で呼び出されるのは BigDecimal.equals() なので、やはり false になります。


対策:BigDecimal の数値比較はこう書く!

BigDecimal で「純粋な数値としての大小・同値」を比較する場合は、equals() ではなく compareTo() を使用するのが鉄則です。

対策1:compareTo() を使用して比較する

compareTo() は、精度(scale)の違いを無視して、数値の純粋な大小関係を比較してくれます。

  • 戻り値が 0 :両者の数値が等しい

  • 戻り値が 1(正の数):呼び出し元が大きい

  • 戻り値が -1(負の数):引数側が大きい

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

    // 数値として等しいかどうかを比較(結果:true)
    if (a.compareTo(b) == 0) {
    System.out.println("数値として同等です");
    }

さきほどの手数料計算ロジックも、以下のように修正することで解決しました。

// compareTo を使って数値の比較を行う
if (magnification != null && DEFAULT_MAGNIFICATION.compareTo(magnification) == 0) {
    return BigDecimal.ZERO;
}

対策2:null が含まれる場合の比較処理

compareTo() は null に対して呼び出すと NullPointerException が発生します。
null の可能性がある場合は、事前に null チェックを行うか、ユーティリティメソッドを作成するのが安全です。

public static boolean isEqual(BigDecimal a, BigDecimal b) {
    if (a == b) return true;
    if (a == null || b == null) return false;
    return a.compareTo(b) == 0;
}

失敗から得た教訓・まとめ

メソッド 比較対象 「1.0」と「1.00」の比較結果 主な用途
equals() 数値 + 精度(scale) false(不一致) Mapのキーなど、精度も含めて完全一致させたい場合
compareTo() 数値のみ(精度は無視) 0(一致) 金額や数量など、通常の数値比較

double の丸め誤差を防ぐために導入した BigDecimal でしたが、言語仕様の理解が浅いまま「いつもの感覚」で equals() を使ったことが事故の原因でした。

  • BigDecimal の数値比較には compareTo() == 0 を使う
  • equals() は精度(scale)まで判定されるため、数値比較に使わない

金額や数値を扱う重要なロジックでは、オブジェクトの同値性と数値としての等価性の違いを常に意識して実装していきましょう!

株式会社ONE WEDGE

【ITエンジニアに、IT業界に貢献する企業】
株式会社ONE WEDGEは、Webシステム開発・SES・AI/DX支援を行うIT企業です。生成AIを活用した業務効率化や次世代システム開発にも注力しており、企業の課題解決だけでなく、エンジニア一人ひとりの成長にも本気で向き合っています。また、技術は「一人で学ぶもの」ではなく「仲間と成長するもの」と考え、社内外でのコミュニティづくりにも力を入れています。
https://onewedge.co.jp/

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?