Java | Java 標準ライブラリ:equals の正しい使い方

Java Java
スポンサーリンク

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 もオーバーライドしないといけません。

理由は、HashSetHashMap のような「ハッシュベースのコレクション」が

hashCode を使って要素を探す
equals を使って最終的な等しさを確認する

という二段階で動いているからです。

ルールはこうです。

equals で true になる2つのオブジェクトは、必ず同じ hashCode を返さなければならない

これを破ると、HashSetHashMap に入れたときに

入れたはずの要素が取り出せない
重複チェックがうまく動かない

といった、非常に気づきにくいバグが起きます。

悪い例をイメージしてみる

もし 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!!
}
Java

name が 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 じゃない場合に
ClassCastExceptionNullPointerException が起きます。

正しくは、型チェックと 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 を使うコード側の「正しい付き合い方」

コレクションでの検索・重複チェック

ListSetMap などのコレクションの中では、
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 を呼ぶと安全

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