equals をオーバーライドする意味
equals は「同じモノかどうか」を値の観点で判定するためのメソッドです。デフォルトの equals は「同じインスタンス(同一性)」しか比較しませんが、値オブジェクトやエンティティでは「中身(等価性)」で比較したい場面が多くあります。そのときに equals をオーバーライドして、何をもって“同じ”とするかをクラスの中に明確に定義します。
守るべき契約(とても重要)
equals の5つの性質
equals をオーバーライドしたら、次の性質を必ず満たす必要があります。これが破られると、コレクションやキャッシュなどで不具合が起きます。
- 反射律(自分自身と等しい): x.equals(x) は常に true
- 対称律(対称に等しい): x.equals(y) が true なら y.equals(x) も true
- 推移律(連鎖して等しい): x.equals(y), y.equals(z) が true なら x.equals(z) も true
- 一貫性(結果がぶれない): 比較対象が変わらなければ、何度呼んでも同じ結果
- 非 null(null とは常に等しくない): x.equals(null) は常に false
加えて「equals をオーバーライドしたら hashCode も必ずオーバーライド」します。同じ値で等しいなら、同じハッシュ値でなければなりません。これが HashMap/HashSet の前提です。
基本パターンと実装テンプレート
値オブジェクト向け(推奨:同一クラスのみ等価)
「同じクラスのインスタンス同士」でだけ比較し、比較対象のフィールドを使って判定します。getClass を使うと、サブクラスとの混在比較を避けられ、対称律を守りやすいです。
import java.util.Objects;
public final class Email {
private final String value;
public Email(String raw) {
var v = raw == null ? "" : raw.trim().toLowerCase(java.util.Locale.ROOT);
if (!v.contains("@")) throw new IllegalArgumentException("invalid email");
this.value = v;
}
@Override public boolean equals(Object o) {
if (this == o) return true; // 同一参照
if (o == null || getClass() != o.getClass()) return false; // 同一クラスのみ
Email other = (Email) o;
return Objects.equals(this.value, other.value); // 値で比較
}
@Override public int hashCode() {
return Objects.hash(value); // 比較に使ったフィールドでハッシュ
}
}
Java継承を見越す場合(instanceof を使う)
継承階層で「親型どうしも等しくしたい」なら instanceof を使います。ただし、対称律が崩れやすいので、親子で同じ比較ルールを厳密に共有できる場合に限定してください。
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Shape s)) return false; // 親型での比較
return Double.compare(this.area(), s.area()) == 0; // 同じ指標で比較(例)
}
Java典型的な落とし穴(重要ポイントの深掘り)
hashCode を忘れる
equals を正しく作っても、hashCode を未定義だと HashSet/HashMap で同一扱いにならず、重複が入ったり見つからなくなります。equals に使ったフィールドと同じセットで hashCode を計算するのが鉄則です。
可変フィールドで比較する
等価に使うフィールドが後から書き換えられると、一度コレクションに入れた後にハッシュが変わり、取り出せなくなる問題が起きます。等価に使うフィールドは不変(final)にするか、格納後に変わらない設計にしてください。
継承で対称律が崩れる
親が instanceof で広く等価判定し、子が getClass で狭く比較すると、x.equals(y) は true だが y.equals(x) は false といった不整合が生まれます。継承で等価判定を共有するのは難度が高いので、値オブジェクトは原則 final クラスにして getClass 方式を推奨します。
例題で身につける
例 1: 金額と通貨の値オブジェクト(不変・安全)
import java.util.Objects;
public final class Money {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
if (amount < 0) throw new IllegalArgumentException("amount>=0");
if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
this.amount = amount;
this.currency = currency.trim();
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money other = (Money) o;
return amount == other.amount && currency.equals(other.currency);
}
@Override public int hashCode() {
return Objects.hash(amount, currency);
}
}
Java同じ金額・同じ通貨なら等しい。不変なので、HashSet に入れても安全です。
例 2: コレクションのキーにする(equals/hashCode の効果)
var m1 = new Money(100, "JPY");
var m2 = new Money(100, "JPY");
java.util.Set<Money> set = new java.util.HashSet<>();
set.add(m1);
System.out.println(set.contains(m2)); // true(値が同じなら見つかる)
Javaequals/hashCode が正しいから、別インスタンスでも“同じ値”として扱えます。
例 3: record を使った簡潔な等価
record は equals/hashCode/toString を自動生成します。検証が必要ならコンパクトコンストラクタで定義します。
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) throw new IllegalArgumentException("non-negative");
}
}
// Point は値で等価。新規で equals/hashCode を書く必要なし
Java実務の指針とチェックリスト
等価に使うフィールドを決める
「何をもって同じとするか」を一度決めたら、コードとテストで固定します。値オブジェクトなら“全フィールド”、エンティティなら“識別子(ID)”のみ、のように設計意図に合わせて選びます。
不変に寄せる
等価に使うフィールドが可変だとトラブルの元です。private final を基本にし、変更したい場合は新インスタンスを返すスタイルへ寄せます。
equals と hashCode をセットで
equals を作ったら、同じフィールド群で hashCode を必ずオーバーライド。Objects.hash を使うと簡潔で安全です。
テストする
反射律・対称律・推移律をテストに落とし込みます。HashSet/HashMap で contains/get が期待どおりに動くかも確認すると安心です。
仕上げのアドバイス(重要部分のまとめ)
equals のオーバーライドは「値で同じか」をクラスに刻む行為です。契約(反射・対称・推移・一貫性・非 null)を守り、hashCode を同じ基準で必ず実装する。不変に寄せてコレクションでも安全に使えるようにし、継承では安易に広げず、原則 final+getClass 比較で対称律を守る——この型を徹底すれば、等価判定のバグは激減し、コレクションやキャッシュで安心して利用できます。
