Java 逆引き集 | equals / hashCode の正しい実装 — コレクションでの動作保証

Java Java
スポンサーリンク

equals / hashCode の正しい実装 — コレクションでの動作保証

コレクション(HashSet/HashMap/HashTableなど)で正しく動かすには、equals と hashCode を「契約どおり」に実装する必要があります。ここを外すと、重複が消えない、取り出せない、キーが壊れるなどの深刻な不具合につながります。初心者向けに、守るべきルールと正しい書き方を例題つきでまとめます。


契約ルール(必ず守るべき原則)

  • 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: x.equals(null) は常に false
  • hashCode の契約:
    • 等価なら同じハッシュ: x.equals(y) が true なら、x.hashCode() == y.hashCode()
    • 一貫性: オブジェクトの状態が変わらない限り、何度呼んでも同じ値
    • 非等価のハッシュは異なる可能性: 異なるオブジェクトが同じハッシュ値になることはあり得る(衝突)。ただし衝突は少ないほど良い。

重要: equals をオーバーライドしたら、必ず hashCode もオーバーライドする。


正しい実装パターン(基本形)

import java.util.Objects;

public final class User {
    private final int id;
    private final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = Objects.requireNonNull(name);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;                 // 同一参照
        if (!(o instanceof User)) return false;     // 型チェック(継承しない価値型なら instanceof)
        User other = (User) o;
        return id == other.id && Objects.equals(name, other.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);              // equals に使ったフィールドのみで計算
    }
}
Java
  • 型チェック: 継承を許すなら instanceof、継承を禁止する final クラスなら getClass でも可。
  • 比較対象の一致: equals に使うフィールドと、hashCode の材料は同じにする。
  • Objects.equals / Objects.hash: null 安全かつ簡潔。初心者はまずこれでOK。

例題で身につける

例題1: HashSet での重複判定

import java.util.*;

var set = new HashSet<User>();
set.add(new User(1, "Tanaka"));
set.add(new User(1, "Tanaka")); // equals が true、hashCode も同じ → 重複扱い
System.out.println(set.size()); // 1
Java
  • 狙い: equals/hashCode の契約が守られていれば、重複要素は増えない。

例題2: HashMap のキー動作

import java.util.*;

var map = new HashMap<User, String>();
var key = new User(2, "Sato");
map.put(key, "DATA");

System.out.println(map.get(new User(2, "Sato"))); // equals/hashCode が一致 → DATA
Java
  • 狙い: 新しい等価キーでも値が取り出せる。

例題3: 大文字小文字を無視した等価(正規化)

import java.util.Objects;

public final class Email {
    private final String value;

    public Email(String value) {
        this.value = Objects.requireNonNull(value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Email)) return false;
        Email other = (Email) o;
        return value.equalsIgnoreCase(other.value);         // equalsIgnoreCase
    }

    @Override
    public int hashCode() {
        return value.toLowerCase().hashCode();              // 正規化と一致させる
    }
}
Java
  • ポイント: equals の比較方法に合わせ、hashCode も同じ規則(正規化)で計算する。

よくある落とし穴と回避策

  • equals をオーバーライドして hashCode を忘れる
    • 対策: equals を作ったら必ず hashCode をセットで作る。IDEの自動生成を活用。
  • 比較フィールドが一致していない
    • 対策: equals と hashCode で使うフィールドは同じにする。
  • 可変フィールドをハッシュに使う
    • 問題: HashSet/HashMap に入れた後に値が変わると見つからなくなる。
    • 対策: できるだけ不変(final)フィールドだけで equals/hashCode を構成する。
  • 継承階層での対称性破壊
    • 問題: 親 equals は instanceof で比較、子 equals は getClass で比較など混在すると対称性が崩れる。
    • 対策: 階層全体で一貫した方針にする。複雑なら不変・final クラス化で継承を禁止。
  • compareTo と equals の不整合
    • 問題: TreeSet/TreeMap は compareTo/Comparator に基づく重複判定。equals とルールが違うと混乱。
    • 対策: 可能なら compareTo と equals の「等価条件」を揃える。

すぐ使えるテンプレート

価値型(不変)におすすめの骨格

import java.util.Objects;

public final class Value {
    private final 型 a;
    private final 型 b;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Value)) return false;
        Value other = (Value) o;
        return Objects.equals(a, other.a) && Objects.equals(b, other.b);
    }

    @Override
    public int hashCode() {
        return Objects.hash(a, b);
    }
}
Java

getClass を使う(継承禁止の厳密比較)

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    var other = (ThisClass) o;
    // フィールド比較...
}
Java

Java record(Java 16+)

public record Point(int x, int y) { }
// record は equals/hashCode/toString を適切に自動生成
Java

まとめ

  • 契約重視: equals と hashCode の契約を守ることが、HashSet/HashMap などの正しい動作の前提。
  • 同一フィールド使用: equals と hashCode は同じフィールドで判定・計算する。
  • 可変を避ける: 不変フィールドを中心に設計し、状態変更でコレクションが壊れないようにする。
  • 自動生成活用: IDEや record を使うと、ミスを減らせる。

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