equals は「同じインスタンスか?」ではなく「意味として同じか?」を見る
equals は
「この2つのオブジェクトは“意味として”同じものと言えるか?」
を判定するメソッドです。
ここでまず大事なのが== と equals は全然違う
ということです。
== は「同じ場所にある同じインスタンスか?」equals は「意味として同じと考えていいか?」
を判定します。
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false(別インスタンス)
System.out.println(a.equals(b)); // true(文字列としては同じ)
Javaオブジェクト指向では
「値として同じなら同じとみなしたい」場面が多いので、
値オブジェクトや独自クラスでは equals を正しく使う・実装することがとても重要になります。
なぜ equals をオーバーライドする必要があるのか(重要)
デフォルトの equals は「==」とほぼ同じ
何もオーバーライドしていないクラスの equals は、Object クラスの実装が使われます。
これは基本的に
this == obj
Javaと同じ判定です。
つまり「同じインスタンスかどうか」しか見ていません。
例えばこういうクラスを考えてみます。
public class UserId {
private final String value;
public UserId(String value) {
this.value = value;
}
}
Javaこれを使ってみると、
UserId id1 = new UserId("U001");
UserId id2 = new UserId("U001");
System.out.println(id1 == id2); // false
System.out.println(id1.equals(id2)); // これも false(デフォルト実装だと)
Java"U001" という中身は同じなのに、別インスタンスなので等しくないことになります。
でも「ユーザID」という値オブジェクトとして考えると、"U001" 同士は「同じユーザID」とみなしたいですよね。
そこで equals をオーバーライドして
「中身の value が同じなら equals は true」
と定義し直す必要が出てきます。
equals をオーバーライドするときの基本パターン
典型的な実装テンプレート
さっきの UserId を「値としての等しさ」で比較できるようにしてみます。
public class UserId {
private final String value;
public UserId(String value) {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("UserId は必須");
}
this.value = value;
}
public String value() {
return value;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1. 同じインスタンスなら true
if (!(o instanceof UserId)) return false; // 2. 型が違えば false
UserId other = (UserId) o; // 3. キャスト
return this.value.equals(other.value); // 4. 中身の値で比較
}
@Override
public int hashCode() {
return value.hashCode();
}
}
Javaこれで
UserId id1 = new UserId("U001");
UserId id2 = new UserId("U001");
UserId id3 = new UserId("U002");
System.out.println(id1.equals(id2)); // true
System.out.println(id1.equals(id3)); // false
Javaのように、「ユーザIDとして同じかどうか」を判定できるようになります。
このパターンは値オブジェクトの equals 実装ではほぼ定番です。
equals と hashCode は絶対にセットで考える(超重要)
契約:「等しいなら同じ hashCode を返せ」
equals をオーバーライドしたら、
必ず hashCode もオーバーライドしないといけません。
理由は、HashSet や HashMap のような「ハッシュベースのコレクション」が
hashCode を使って要素を探すequals を使って最終的な等しさを確認する
という二段階で動いているからです。
ルールはこうです。
equals で true になる2つのオブジェクトは、必ず同じ hashCode を返さなければならない
これを破ると、HashSet や HashMap に入れたときに
入れたはずの要素が取り出せない
重複チェックがうまく動かない
といった、非常に気づきにくいバグが起きます。
悪い例をイメージしてみる
もし UserId で equals だけオーバーライドして、hashCode を変えなかったらどうなるか。
Set<UserId> set = new HashSet<>();
set.add(new UserId("U001"));
System.out.println(set.contains(new UserId("U001")));
Java中身としては「同じユーザID」のはずですが、
hashCode がバラバラだと contains が false になることがあります。
だからこそ、equals と hashCode は「運命共同体」です。
片方だけいじる、は絶対にやってはいけません。
equals を正しく使うときの注意ポイント
null 比較のときは呼び出し順に注意
よくあるのが、こんなコードです。
String name = null;
if (name.equals("Taro")) {
// NullPointerException!!
}
Javaname が null の場合、name.equals(...) は NPE になってしまいます。
こういうときは、「null にならない側」から equals を呼ぶのが安全です。
if ("Taro".equals(name)) {
// OK。name が null でも安全
}
Javaこれなら "Taro" は絶対に null ではないので、equals 呼び出しで例外は起こりません。
== と equals の使い分け
参照型については、原則こう考えてください。
「同じインスタンスかどうか」を見たいとき → ==
「値として等しいかどうか」を見たいとき → equals
String や自作の値オブジェクトは、意味としての等しさを見たいことがほとんどなので、== ではなく equals を使います。
String s1 = "abc";
String s2 = new String("abc");
System.out.println(s1 == s2); // false の可能性がある
System.out.println(s1.equals(s2)); // true
Javaプリミティブ型(int, long, boolean など)は == で比較します。
そもそも equals メソッドを持っていません。
equals が守るべき性質(少しだけ真面目な話)
equals の「契約」
Java 言語仕様では、equals は次の性質を満たすべきとされています。
自反的(reflexive)
任意の x に対して x.equals(x) は true
対称的(symmetric)x.equals(y) が true なら y.equals(x) も true
推移的(transitive)x.equals(y) と y.equals(z) が両方 true なら、x.equals(z) も true
一貫性(consistent)
同じオブジェクト同士なら、何度呼んでも結果が変わらない(途中で状態を変えない限り)
null との比較
どんな x に対しても、x.equals(null) は必ず false
初心者の段階では、
「同じものは必ず同じになる」
「違うものが状況によってコロコロ変わらない」
くらいに覚えておけば十分です。
ただ、equals の中で
毎回変わるもの(現在時刻、乱数など)を比較に使ってしまう
片方だけで true / false が変わるような変なロジックを書く
と、この契約を破ってしまう可能性があるので注意が必要です。
実際にやりがちなミスと、その直し方
ミス1:instanceof を忘れて、キャストだけしてしまう
悪い例:
@Override
public boolean equals(Object o) {
UserId other = (UserId) o;
return this.value.equals(other.value);
}
Javaこれは、o が null だったり、そもそも UserId じゃない場合にClassCastException や NullPointerException が起きます。
正しくは、型チェックと this == o チェックを必ず挟みます。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof UserId)) return false;
UserId other = (UserId) o;
return this.value.equals(other.value);
}
Javaミス2:比較すべきフィールドを間違える/足りない
例えば User クラスが
id
name
email
を持っているとして、
equals で id だけを比較するのか、
id と email を比較するのか、
全フィールドを比較するのか、
は「何をもって同じユーザとみなすか」の設計の話です。
「そのドメインにとっての“同一性”」をよく考えずに
「とりあえず全部比較しとくか」で equals を書いてしまうと、
後から「id だけで良かったのに…」と後悔することもあります。
値オブジェクト(UserId, EmailAddress, Money など)は
「そのオブジェクトの意味を決めるフィールドだけ」を equals の対象にします。
equals を使うコード側の「正しい付き合い方」
コレクションでの検索・重複チェック
List や Set、Map などのコレクションの中では、
equals がかなり頻繁に使われています。
List.contains(x)Set.add(x)(重複チェック)Map.get(key)
などは、内部で equals を呼んで比較します。
例えば Set<UserId> に重複なくユーザIDを入れたいとき、UserId の equals / hashCode が正しく実装されていれば
Set<UserId> ids = new HashSet<>();
ids.add(new UserId("U001"));
ids.add(new UserId("U001")); // これは無視される(重複)
Javaのように、自然な形で使えます。
独自クラスをキーにするときは特に注意
Map<UserId, User> のように、
自作クラスをキーに Map を使う場合は、
ほぼ必ず equals / hashCode の正しい実装が必要になります。
ここをサボると、入れたはずの値が取り出せない、という
訳の分からない挙動になりがちです。
まとめ:初心者がまず押さえるべき equals のポイント
最後に、あなたが今意識しておくといいポイントをまとめます。
== は「同じインスタンスか」、equals は「意味として同じか」
値オブジェクト(ID、メールアドレス、金額など)では equals をオーバーライドする
equals をオーバーライドしたら、hashCode も必ずセットでオーバーライドする
equals 実装では、this == o チェック → instanceof チェック → キャスト → フィールド比較、という流れが定番
null 比較では "Taro".equals(name) のように「null にならない側」から equals を呼ぶと安全
