JavaのBigDecimalで0.0と0.00を比較する方法を解説する記事のアイキャッチ。Java道場風の空間で若手エンジニアがequalsとcompareToの違いを学ぶイラスト。

Java

JavaのBigDecimalでequalsがfalseになる理由|0.0と0.00はcompareToで比較する?

AIによる要約

BigDecimal.equalsは数値だけでなく小数点以下の桁数を表すscaleも比較するため、0.00.00はfalseになります。金額などを数値として比較するならcompareToの結果が0かを確認します。ただし、HashSetやHashMapはequalsとhashCodeを使うため、compareToへ置き換えればすべて解決するわけではありません。入力・保存・表示のどこで桁数を統一するかを業務ルールとして決めることが重要です。

新人SE
新人SE
DBには0.00が入っているのに、BigDecimal.ZEROとequalsで比較したらfalseになりました。値は同じに見えるのですが……。
ポンコツSE
ポンコツSE
BigDecimalでは「数値として同じ」と「値と桁数まで同じ」が別です。まずequalsとcompareToの役割を分けて確認しましょう。

販売管理や請求、会計連携などの業務システムでは、小数を正確に扱うためにBigDecimalをよく使います。ところが、見た目には同じ金額なのに比較結果がfalseになり、条件分岐へ入らないことがあります。

原因を「BigDecimalがおかしい」で終わらせると、別の画面やバッチでも同じ不具合を繰り返します。この記事では、equalscompareToの違いから、HashSet・DB・テストでの注意点まで、新人がレビュー前に判断できる形で整理します。

この記事のポイント

  • equalsは値とscaleの両方を比較する
  • 数値として等しいかはcompareTo(...) == 0で判定する
  • HashSetやHashMapではequals・hashCodeのルールが使われる
  • 金額の桁数と丸めは業務ルールとして境界で統一する

0.0と0.00をequalsで比較するとfalseになる

BigDecimal left = new BigDecimal("0.0");
BigDecimal right = new BigDecimal("0.00");

System.out.println(left.equals(right));          // false
System.out.println(left.compareTo(right) == 0); // true

0.00.00は数値としては同じです。しかし、BigDecimalは小数点以下の桁数も状態として持っています。この桁数がscaleです。

unscaled valuescale
0.001
0.0002

equalsは数値とscaleが両方一致するときだけtrueを返します。一方、compareToは数値上の大小を比べるため、scaleが違っても数値が同じなら0を返します。

金額が同額かを確認したいならcompareTo、桁数を含めて完全に同じ状態かを確認したいならequals、と目的から選びます。

BigDecimal.ZEROでも同じ問題が起きる

BigDecimal.ZEROのscaleは0です。DBのDECIMAL列や画面入力から取得した値が0.00なら、次の判定はfalseになる可能性があります。

BigDecimal amount = new BigDecimal("0.00");

// NG: scaleが違うとfalse
if (BigDecimal.ZERO.equals(amount)) {
    System.out.println("金額は0円です");
}

// OK: 数値として0かを判定
if (amount != null && amount.compareTo(BigDecimal.ZERO) == 0) {
    System.out.println("金額は0円です");
}

現場でありがちなのは、単体テストではBigDecimal.ZEROを使って成功し、結合テストでDBから0.00が返ったときだけ失敗するケースです。テストデータも本番列のscaleに近い値で用意すると、この差を早く見つけられます。

compareToの結果は0と比較する

compareToは「同じなら0」「左が小さければ負」「左が大きければ正」を返します。戻り値が必ず-1、0、1だけとは限らない前提で、大小は0との比較で書きます。

BigDecimal amount = new BigDecimal("1200.00");
BigDecimal limit = new BigDecimal("1000");

if (amount.compareTo(limit) > 0) {
    System.out.println("上限を超えています");
}

if (amount.compareTo(limit) >= 0) {
    System.out.println("上限以上です");
}
判定書き方
等しいa.compareTo(b) == 0
aが小さいa.compareTo(b) < 0
aが大きいa.compareTo(b) > 0
aがb以上a.compareTo(b) >= 0

nullを含む比較は先に方針を決める

compareToを呼ぶ対象がnullならNullPointerExceptionになります。ここで安易にnullを0へ置き換えると、「金額未入力」と「0円」を同じ扱いにしてしまう可能性があります。

public boolean isZero(BigDecimal amount) {
    if (amount == null) {
        return false;
    }
    return amount.compareTo(BigDecimal.ZERO) == 0;
}

nullを許可しない項目なら、Formの入力チェックやコンストラクタで拒否する方が明確です。nullを0と解釈する仕様なら、その変換場所と理由をメソッド名やコメントで示します。比較式だけで業務仕様を隠さないことが重要です。

doubleからBigDecimalを作らない

比較方法が正しくても、生成時点で意図しない小数が入っていれば期待通りになりません。

BigDecimal ng = new BigDecimal(0.1);

BigDecimal okFromString = new BigDecimal("0.1");
BigDecimal okFromDouble = BigDecimal.valueOf(0.1);

System.out.println(ng);
System.out.println(okFromString);

new BigDecimal(0.1)は、doubleが持つ2進数上の近似値をそのまま受け取ります。固定値や文字列入力なら文字列コンストラクタを使い、doubleから変換せざるを得ない場合はBigDecimal.valueOfを検討します。

setScaleで揃えればよいとは限らない

比較前にすべてsetScale(2)すればequalsでも一致しますが、丸め方法を指定せず桁を減らすとArithmeticExceptionになることがあります。また、税率や重量など、項目によって必要な小数桁は異なります。

BigDecimal unitPrice = new BigDecimal("123.456");

BigDecimal normalized = unitPrice.setScale(
        2,
        RoundingMode.HALF_UP);

System.out.println(normalized); // 123.46

scaleを統一するなら、「金額は小数2桁」「端数は四捨五入」などの仕様に基づき、入力受付時、計算結果、DB保存前といった境界で行います。比較するたびにその場しのぎで丸めると、計算結果が処理ごとに変わります。

HashSetとHashMapではcompareToに置き換わらない

HashSetの重複判定やHashMapのキー検索は、equalshashCodeを使います。そのため、数値として同じBigDecimalでも別要素として扱われることがあります。

Set<BigDecimal> amounts = new HashSet<>();
amounts.add(new BigDecimal("10.0"));
amounts.add(new BigDecimal("10.00"));

System.out.println(amounts.size()); // 2
System.out.println(amounts.contains(new BigDecimal("10"))); // false

「金額として同じなら1件にしたい」のであれば、登録前に業務上のscaleへ正規化する、金額を表す値オブジェクトで等価性を定義するなど、コレクションへ入れる前の設計が必要です。

TreeSetなら同じ動きになるとは限りません

TreeSetは並び順の比較にcompareToを使うため、10.0と10.00を同じ要素として扱うことがあります。同じ値の集合でもHashSetと件数が変わり得ます。コレクションの種類だけを変更して不具合を隠さず、等価性の要件を確認してください。

現場での確認順

  1. 比較対象をログやデバッガーで確認する
  2. toPlainString()だけでなくscale()も確認する
  3. 同額判定か、桁数まで含む一致判定かを仕様で確認する
  4. DB列、Form、外部APIの小数桁を確認する
  5. HashSetやHashMapのキーとして使っていないか確認する
  6. 正規化するなら場所と丸め規則を決める
log.debug(
        "amount={}, scale={}, limit={}, limitScale={}",
        amount.toPlainString(),
        amount.scale(),
        limit.toPlainString(),
        limit.scale());

本番ログへ金額を出せるかは情報管理ルールに従います。調査のために無条件で顧客情報や取引情報を記録しないでください。

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

// レビューコメント例
金額の同額判定にBigDecimal.equalsを使っているため、
DBからscale=2で取得した0.00を0円と判定できません。
compareToで数値比較する意図か確認してください。

// レビューコメント例
比較のたびにsetScaleしており、丸め規則が複数箇所へ散っています。
金額を確定する境界でscaleを統一できないか検討してください。

// レビューコメント例
nullを0へ変換していますが、未入力と0円は同じ仕様でしょうか。
画面・DBの必須条件を確認してください。

提出前のセルフチェック

  • 同額判定とscaleまで含む一致判定を区別したか
  • 数値比較をcompareTo(...) == 0で書いたか
  • nullを0とみなしてよい仕様か確認したか
  • new BigDecimal(double)を使っていないか
  • 丸め規則とscaleの統一場所が決まっているか
  • HashSet・HashMapのキーで使う場合の等価性を確認したか
  • DBと同じ小数桁のテストデータを含めたか

Javaの値比較を体系的に学ぶ参考書

BigDecimalだけでなく、equals・hashCode・コレクションの契約まで一緒に学ぶと、比較処理のレビューで判断しやすくなります。

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

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

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

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

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

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

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

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

まとめ

BigDecimal.equalsは数値とscaleを比較するため、0.0と0.00はfalseになります。業務上の同額判定にはcompareTo(...) == 0を使い、完全一致が必要な場面だけequalsを選びます。

ただし、HashSet・HashMapやDB保存まで含めると、比較式だけの修正では足りません。入力、計算、保存のどこで桁数と丸めを確定するかをチームで決め、実データに近いscaleを使ってテストしましょう。

-Java
-, , , ,