はじめに
Javaでオブジェクトを比較するとき、== ではなく equals() を使うべきというのはよく知られています。しかし「なぜオーバーライドが必要なのか」「hashCode() と何の関係があるのか」まで説明できる人は少ないかもしれません。
この記事では、オーバーライドしなかった場合に何が起きるかをコードで確認しながら、正しい実装パターンまでを解説します。
equals() をオーバーライドしないと何が起きるか
まず、オーバーライドしていないクラスを用意します。
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
同じ値を持つ2つのインスタンスを比較してみます。
User user1 = new User("田中", 25);
User user2 = new User("田中", 25);
System.out.println(user1.equals(user2)); // false
中身は同じなのに false が返ります。
なぜ false になるのか
equals() をオーバーライドしていない場合、Object クラスのデフォルト実装が使われます。
// Object クラスのデフォルト実装(イメージ)
public boolean equals(Object obj) {
return (this == obj); // 参照先が同じかどうかの比較
}
user1 と user2 は別々に new したオブジェクトなので、参照先が異なります。そのため中身が同じでも false になります。
equals() をオーバーライドする
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age && Objects.equals(name, user.name);
}
User user1 = new User("田中", 25);
User user2 = new User("田中", 25);
System.out.println(user1.equals(user2)); // true
hashCode() をオーバーライドしないと何が起きるか
equals() だけをオーバーライドして hashCode() を放置すると、HashSet や HashMap で問題が起きます。
// equals() だけオーバーライドして hashCode() はデフォルトのまま
Set<User> userSet = new HashSet<>();
userSet.add(new User("田中", 25));
userSet.add(new User("田中", 25)); // 同じ内容のUser
System.out.println(userSet.size()); // 2 ← 重複として扱われていない
HashSet は「同じ要素の重複を許さない」コレクションです。しかし同じ内容の User が2件入ってしまっています。
なぜこうなるのか
HashSet の内部動作を順番に追います。
① 要素を追加するとき、まず hashCode() でバケット(格納場所)を決める
② 同じバケットに要素があれば equals() で同一かどうか確認する
③ equals() が true なら重複とみなし、追加しない
hashCode() をオーバーライドしていないと、同じ内容のオブジェクトでも異なるハッシュ値が返ります。すると①の時点で別のバケットに格納されるため、②の equals() 比較までたどり着けません。
User user1 = new User("田中", 25);
User user2 = new User("田中", 25);
System.out.println(user1.hashCode()); // 例:1829164700
System.out.println(user2.hashCode()); // 例:2018699554 ← 別の値
hashCode() をオーバーライドする
@Override
public int hashCode() {
return Objects.hash(name, age);
}
Set<User> userSet = new HashSet<>();
userSet.add(new User("田中", 25));
userSet.add(new User("田中", 25));
System.out.println(userSet.size()); // 1 ← 正しく重複と判定される
equals() と hashCode() は必ずセットでオーバーライドする
Javaには以下の契約(Contract)があります。
equals()がtrueを返す2つのオブジェクトは、必ず同じhashCode()を返さなければならない
これを守るために、equals() をオーバーライドしたら 必ず hashCode() もオーバーライドします。
Lombokを使う場合
Lombokの @EqualsAndHashCode を使うと、両方を自動生成できます。
@EqualsAndHashCode
public class User {
private String name;
private int age;
}
特定のフィールドだけを比較対象にしたい場合は以下のように指定します。
@EqualsAndHashCode(of = {"name"}) // name だけで比較
public class User {
private String name;
private int age;
}
HashMap でも同じ問題が起きる
HashSet と同様に、HashMap のキーとしてオブジェクトを使う場合も注意が必要です。
// hashCode() をオーバーライドしていない場合
Map<User, String> map = new HashMap<>();
User user1 = new User("田中", 25);
map.put(user1, "エンジニア");
User user2 = new User("田中", 25); // 同じ内容
System.out.println(map.get(user2)); // null ← 取得できない
hashCode() が異なるため、user1 を格納したバケットと異なる場所を探してしまい null が返ります。
まとめ
| 状況 | 結果 |
|---|---|
equals() も hashCode() もオーバーライドしない |
参照比較になる、コレクションで重複が検出されない |
equals() だけオーバーライド |
値比較はできるが、HashSet・HashMap で正しく動かない |
| 両方オーバーライド | 値比較・コレクションの動作ともに正しく動く |
equals() と hashCode() は必ずセットでオーバーライドするというルールを守れば、今回のような問題は防げます。IDEやLombokの自動生成を用いれば効率よく実装できます。