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

理由は、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!!
}
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 じゃない場合に
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 を呼ぶと安全

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