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);
}
}
JavagetClass を使う(継承禁止の厳密比較)
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
var other = (ThisClass) o;
// フィールド比較...
}
JavaJava record(Java 16+)
public record Point(int x, int y) { }
// record は equals/hashCode/toString を適切に自動生成
Javaまとめ
- 契約重視: equals と hashCode の契約を守ることが、HashSet/HashMap などの正しい動作の前提。
- 同一フィールド使用: equals と hashCode は同じフィールドで判定・計算する。
- 可変を避ける: 不変フィールドを中心に設計し、状態変更でコレクションが壊れないようにする。
- 自動生成活用: IDEや record を使うと、ミスを減らせる。
