値オブジェクトの「比較」とは何を意味するか
値オブジェクトは
「中身の値が同じなら、同じものとして扱うクラス」
です。
お金 1000円、メールアドレス x@example.com、日付 2025-01-01 など、
「誰か」ではなく「いくら」「どれ」という“値そのもの”を表します。
だから比較するときも、「同じインスタンスか?」ではなく
「中身の値が同じか?」をちゃんと見る必要があります。
そのために equals と hashCode を“正しく”オーバーライドするのがとても重要です。
例題 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のように、自然な大小比較ができます。
equals と compareTo が矛盾しないように(equals で true なら compareTo は 0)定義することも大事なポイントです。
まとめ:値オブジェクトの比較で意識すること
値オブジェクトの比較は、本質的に「中身の値が同じかどうか」の判定です。
そのために、
== ではなく equals をオーバーライドして使うequals を書いたら必ず hashCode もセットで書く
比較に使うフィールドは「その値オブジェクトの意味そのもの」に限る
大小比較が必要なら Comparable(compareTo)も実装する
というところを押さえておくと安心です。
