25
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

equals()とhashCode()をオーバーライドしないと何が起きるか

25
Posted at

はじめに

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); // 参照先が同じかどうかの比較
}

user1user2 は別々に 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() を放置すると、HashSetHashMap で問題が起きます。

// 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() だけオーバーライド 値比較はできるが、HashSetHashMap で正しく動かない
両方オーバーライド 値比較・コレクションの動作ともに正しく動く

equals()hashCode()必ずセットでオーバーライドするというルールを守れば、今回のような問題は防げます。IDEやLombokの自動生成を用いれば効率よく実装できます。

25
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?