3-11:equalsをオーバーライドするときは、常にhashCodeをオーバーライドする
要点
equals をオーバーライドしたら、必ず hashCode もオーバーライドする。
等価なオブジェクトは 同じ hashCode() を返さなければならない(これが hashCode の契約)。
守らないと HashMap/HashSet 等のハッシュベースのコレクションが正しく動きません。
なぜか
HashMap や HashSet は内部でハッシュ値(hashCode())を使ってオブジェクトの格納場所(バケット)を決める。
- equals(a,b) が true でも a.hashCode() != b.hashCode() だと、別バケットになり contains/get が失敗することがある。
→ 等価なのに見つからない、というバグの原因になる。
悪い例(問題を再現するコード)
// equals はオーバーライドしたが hashCode をオーバーライドしていない(Object#hashCode のまま)
public class BadPerson {
private final String name;
private final int age;
public BadPerson(String name, int age) { this.name = name; this.age = age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof BadPerson)) return false;
BadPerson p = (BadPerson) o;
return age == p.age && Objects.equals(name, p.name);
}
// hashCode はオーバーライドしていない!
}
// → 使用例
BadPerson a = new BadPerson("Alice", 30);
BadPerson b = new BadPerson("Alice", 30);
Set<BadPerson> set = new HashSet<>();
set.add(a);
set.contains(b); // 期待: true だが、false になる可能性が高い
説明:
a.equals(b) は true でも、a.hashCode() と b.hashCode() が Object の実装(オブジェクト識別に依存)で異なるため、HashSet では別扱いになる。
良い例
public class Person {
private final String name;
private final int age;
public Person(String name, int age) { this.name = name; this.age = age; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Person)) return false;
Person other = (Person)o;
return age == other.age && Objects.equals(name, other.name);
}
@Override
public int hashCode() {
// シンプルで安全:null 護衛もしてくれる
return Objects.hash(name, age);
}
}
これで new Person("A",1).equals(new Person("A",1)) == true のとき、両者は同じ hashCode() を返すためハッシュコレクションは正しく動く。
実務上の注意点(チェックリスト)
-
等しい → hashCode は同じ を常に保証する。逆は不要(異なるオブジェクトが同じハッシュになることは許容される)。
-
equals に使うフィールドは hashCode にも含める(両方に同じフィールドセットを使う)。
-
ミュータブルフィールドを避ける:equals/hashCode にミュータブルなフィールドを使うと、コレクションに入れた後に状態が変わり見つけられなくなる(バグ)。可能なら不変クラスにする。
-
パフォーマンス:Objects.hash は簡便だがやや遅い(小さなホットパスなら手書きの 31×方式が速い)。必要なら hashCode をキャッシュ(不変クラスのみ)する。
-
配列フィールドは Arrays.hashCode / Arrays.deepHashCode を使う(Objects.hash に配列を渡すと配列参照のハッシュになる)。
-
浮動小数点は Double.hashCode / Float.hashCode を使う(== や Double.doubleToLongBits の扱いに注意)。
例:配列・double を含む安全な実装
public final class Item {
private final String name;
private final double value;
private final int[] tags;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Item)) return false;
Item other = (Item)o;
return Double.compare(value, other.value) == 0
&& Objects.equals(name, other.name)
&& Arrays.equals(tags, other.tags);
}
@Override
public int hashCode() {
int result = Objects.hash(name, Double.hashCode(value));
result = 31 * result + Arrays.hashCode(tags);
return result;
}
}