Java | オブジェクト指向:値オブジェクトの比較

Java Java
スポンサーリンク

値オブジェクトの「比較」とは何を意味するか

値オブジェクトは
「中身の値が同じなら、同じものとして扱うクラス」
です。

お金 1000円、メールアドレス x@example.com、日付 2025-01-01 など、
「誰か」ではなく「いくら」「どれ」という“値そのもの”を表します。

だから比較するときも、「同じインスタンスか?」ではなく
「中身の値が同じか?」をちゃんと見る必要があります。
そのために equalshashCode を“正しく”オーバーライドするのがとても重要です。


例題 1: Money 値オブジェクトの equals / hashCode

まずはダメな比較から

何も考えずにこう比較してしまうとします。

Money m1 = new Money(1000);
Money m2 = new Money(1000);

if (m1 == m2) {
    System.out.println("same");
}
Java

== は「同じインスタンス(同じメモリアドレス)かどうか」の比較なので、
new した別インスタンス同士は false になります。

値オブジェクトでは、== ではなく equals を自分で定義して使う必要があります。

Money クラスを値オブジェクトらしく定義する

final class Money {

    private final int amount;   // 金額

    Money(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("金額は0以上");
        }
        this.amount = amount;
    }

    int amount() {
        return amount;
    }

    // 中身の値で比較するための equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;              // 同じインスタンスなら true
        if (!(o instanceof Money)) return false; // 型が違えば false
        Money other = (Money) o;
        return this.amount == other.amount;      // 中身の値で比較
    }

    // equals とセットで hashCode もオーバーライドする
    @Override
    public int hashCode() {
        return Integer.hashCode(amount);
    }
}
Java

こうしておけば、

Money m1 = new Money(1000);
Money m2 = new Money(1000);

System.out.println(m1.equals(m2));  // true
Java

となり、「1000円同士は同じ」として扱えます。

equals をオーバーライドしたら、hashCode も必ず一緒にオーバーライドするのが大事なルールです。
「equals で等しいオブジェクトは、必ず同じ hashCode を返さないといけない」という契約があるからです。


equals / hashCode 実装のポイント(重要な深掘り)

equals の基本ルール

自前で equals を書くときは、最低限これを守ります。

this == o なら true を返す
null と比較したら必ず false
型が違うなら false(instanceof でチェック)
「対称的・推移的・一貫している」結果になるようにする

実装パターンはだいたいこうです。

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (!(o instanceof Money)) return false;
    Money other = (Money) o;
    return this.amount == other.amount;
}
Java

この「型チェック+キャスト+フィールド比較」の流れは、値オブジェクトではほぼ定型です。

hashCode の基本ルール

hashCode に関して重要なのは:

equals で等しいオブジェクトは、必ず同じ hashCode を返す
equals で異なるオブジェクトが同じ hashCode を返してもよい(ただし少ないほうがよい)

つまり Money なら、「amount が同じなら hashCode も同じ」にすれば OK です。

@Override
public int hashCode() {
    return Integer.hashCode(amount);
}
Java

これで、HashSet<Money>HashMap<Money, ...> に入れたときも
「1000円」が重複して入ることなく、正しく扱えます。


例題 2: メールアドレス値オブジェクトの比較

String のままだと「何の値か分からない」

メールアドレスを String でそのまま持っていると、

String email1 = "a@example.com";
String email2 = "a@example.com";

email1.equals(email2); // true(これは OK)
Java

比較自体はできますが、「メールアドレスとして不正な文字列」を簡単に混入させてしまいます。

値オブジェクトにすると、不変条件と比較ロジックを一箇所に閉じ込められます。

EmailAddress 値オブジェクト

final class EmailAddress {

    private final String value;

    EmailAddress(String value) {
        if (value == null || !value.contains("@")) {
            throw new IllegalArgumentException("メール形式が不正");
        }
        this.value = value;
    }

    String value() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof EmailAddress)) return false;
        EmailAddress other = (EmailAddress) o;
        return this.value.equals(other.value);
    }

    @Override
    public int hashCode() {
        return value.hashCode();
    }
}
Java

これで、

EmailAddress e1 = new EmailAddress("a@example.com");
EmailAddress e2 = new EmailAddress("a@example.com");

System.out.println(e1.equals(e2));  // true
Java

のように、中身の文字列が同じなら同じと判定できます。
同時に、「@ がない文字列」はそもそも new できないので、不正値の混入を防げます。


compareTo と「大小比較」が欲しい場合

Comparable を実装する

値オブジェクトによっては、「等しいかどうか」だけでなく
「どっちが大きいか(順序づけ)」も必要になることがあります。

例えば Money を「金額の大小でソートしたい」ときは、Comparable を実装します。

final class Money implements Comparable<Money> {

    private final int amount;

    Money(int amount) {
        if (amount < 0) throw new IllegalArgumentException();
        this.amount = amount;
    }

    @Override
    public int compareTo(Money other) {
        return Integer.compare(this.amount, other.amount);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money other = (Money) o;
        return this.amount == other.amount;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(amount);
    }
}
Java

これで、

List<Money> list = List.of(new Money(300), new Money(100), new Money(200));
Collections.sort(list);  // amount の昇順に並ぶ
Java

のように、自然な大小比較ができます。

equalscompareTo が矛盾しないように(equals で true なら compareTo は 0)定義することも大事なポイントです。


まとめ:値オブジェクトの比較で意識すること

値オブジェクトの比較は、本質的に「中身の値が同じかどうか」の判定です。

そのために、

== ではなく equals をオーバーライドして使う
equals を書いたら必ず hashCode もセットで書く
比較に使うフィールドは「その値オブジェクトの意味そのもの」に限る
大小比較が必要なら Comparable(compareTo)も実装する

というところを押さえておくと安心です。

タイトルとURLをコピーしました