プログラミング開発

【Java】equalsとhashCodeを完全ガイド:Lombokでの自動生成と落とし穴も解説

この記事は約7分で読めます。

はじめに

Javaを学び始めて少し経つと、ほとんどの開発者が「equals()hashCode()」の壁にぶつかります。
オブジェクト同士の比較が思った通りに動かない原因の多くが、この2つのメソッドにあります。
最近では Lombok を使って自動生成するケースも増えましたが、仕組みを理解していないと意図せぬバグを生む こともあります。

この記事では、

  1. equals/hashCode の基本
  2. Lombok による自動生成
  3. 注意すべき落とし穴

を実践的にまとめます。

equals/hashCode の基本

equals() と == の違い

Java では ==equals() は、全く異なる動作をします。

比較方法比較内容主な用途
==参照の同一性(同じメモリ上のオブジェクトか)プリミティブ型、シングルトン判定
equals()値の同一性(内容が等しいか)String、独自クラスなどの中身比較
String s1 = new String("apple");
String s2 = new String("apple");

System.out.println(s1 == s2);      // false(別インスタンス)
System.out.println(s1.equals(s2)); // true(中身が同じ)

equals と hashCode の「約束事」

Javaでは次のルールが定められています。

  • equals() が true なら、hashCode() も必ず同じでなければならない
  • hashCode() が同じでも、equals() が false になることはある(衝突は許容)

このため、equals をオーバーライドしたら hashCode も必ず実装する必要があります。

実装(基本形)

以下は、nameとageを持つUserクラスです。このクラスにequals()とhashCode()を実装するといかのようになります。

import java.util.Objects;

public class User {
    private String name;
    private int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return age == user.age && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

これがいわゆる教科書的な実装だけれども、現場ではこのようなコードを毎度手で書くのは面倒…。ここで登場するのが Lombokです!

Lombok で自動生成する方法

Lombokとは

Lombok(ロムボック) は、Javaの冗長なコードを削減するための アノテーションベースのライブラリ です。
Getter・Setter・コンストラクタ・toString・equals・hashCodeなどを自動生成し、開発者が手で書く手間を大幅に減らしてくれます。

Javaのプロジェクトでは基本的につかわれているライブラリだね。equalsとhashCodeを実装するには、Lombokの@Dataをつけるだけでいいから、コンパクトになるよね。

@Data を使うだけでOK

import lombok.Data;

@Data
public class User {
    private String name;
    private int age;
}

@Data をつけると、以下のメソッドが自動生成されます。

  • @Getter / @Setter
  • @RequiredArgsConstructor
  • @ToString
  • @EqualsAndHashCode

つまり、equals と hashCode も自動で実装されるということです。

内部的には、次のようなコードが生成されます。

@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 && (name != null ? name.equals(user.name) : user.name == null);
}

@Override
public int hashCode() {
    int result = name != null ? name.hashCode() : 0;
    result = 31 * result + age;
    return result;
}

Lombokを使うときの注意点

比較に含めたくないフィールドがある場合

例えば、DBの主キー id だけは比較対象にしたくない場合、@EqualsAndHashCode をカスタマイズします。

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@EqualsAndHashCode(of = {"email"}) // email だけで同一判定する
public class User {
    private Long id;
    private String name;
    private String email;
}

このように of 属性を使えば、特定のフィールドのみで比較が可能です。

継承クラスでは注意が必要

デフォルトでは、@EqualsAndHashCodeスーパークラスのフィールドを無視します。
継承関係を考慮したい場合は、callSuper = true を指定します。

@EqualsAndHashCode(callSuper = true)
public class PremiumUser extends User {
    private int rank;
}

③ 可変オブジェクトは比較対象にしない

equals / hashCode の基準に、変更されるフィールド(例:lastLoginTime など)を含めると、
コレクションのキーとして使った際に破壊的なバグを生む可能性があります。

User u = new User("Alice", 25);
Map<User, String> map = new HashMap<>();
map.put(u, "Tokyo");

u.setName("Bob"); // ← hashCodeが変わる!
System.out.println(map.get(u)); // null(キーが見つからない)

このようなケースでは、不変(immutable)オブジェクトを設計するか、比較対象を限定することが推奨です。


equals/hashCode のテストも書こう

Lombokを使っていても、動作保証のための単体テストは重要です。

@Test
void testEqualsAndHashCode() {
    User u1 = new User("Alice", 25);
    User u2 = new User("Alice", 25);

    assertEquals(u1, u2);
    assertEquals(u1.hashCode(), u2.hashCode());
}

Lombokが正しく生成しているか、フィールド変更の影響を確認できます。

あるに越したことはないけれど、@Dataをつけるだけのクラスに対してはテストコードは正直不要かな…。ただof属性をつかった独自実装がある場合はテストを実装すべき!

まとめ

equals()hashCode() は、Javaの中でも理解の浅さがバグにつながりやすい領域です。
Lombokを使えば簡単に書けますが、その仕組みと制約を理解したうえで使うことで、はじめて「安全な自動化」が実現します。

もしチーム開発でLombokを導入しているなら、レビュー時に「どのフィールドで比較しているか」「可変データを含めていないか」を確認するのがベストプラクティスです。

  • equals/hashCode は常にペアで実装するものである
  • Lombok の @Data は自動生成してくれる
  • 特定フィールドだけ比較し、equals/hashCodeを実装できる