Java

Javaのequalsをオーバーライドする注意点

AIによる要約

Javaでequalsをオーバーライドするときは、単にIDEで生成すればよいわけではありません。何を同じとみなすのか、hashCodeと揃っているか、変更される値を比較に使っていないかを確認する必要があります。

新人SE
新人SE
equalsを自動生成したらレビューで止まりました。生成コードなのにダメなんですか?
ポンコツSE
ポンコツSE
生成コード自体が悪いわけではありません。問題は、何を同じとみなすかを決めずに生成していることです。

equalsは、オブジェクト同士を「同じものとして扱うか」を決めるメソッドです。

現場では、ID、コード値、注文番号など、業務上の同一性を表すクラスでよく話題になります。

この記事のポイント

  • equalsは同一判定のルールを表す
  • オーバーライドするならhashCodeもセットで考える
  • 比較に使うフィールドは業務上の意味で決める
  • 可変フィールドを比較に入れるとSetやMapで事故りやすい

equalsは何を同じとみなすかのルール

初学者のうちは、equalsを「オブジェクトを比較するメソッド」とだけ覚えがちです。しかし実務では、もう一歩踏み込んで「何を同じとみなすか」を決める設計になります。

public class ProductCode {
    private final String value;

    public ProductCode(String value) {
        this.value = Objects.requireNonNull(value);
    }
}

このProductCodeであれば、valueが同じなら同じ商品コードとして扱いたいはずです。つまり、equalsの基準はvalueになります。

まず守るべき基本形

equalsを手書きする場合は、自己比較、型チェック、フィールド比較の順で考えると読みやすくなります。

equalsをオーバーライドするときの確認順序を、同一性、自己比較、型チェック、フィールド比較、hashCode確認の順で示す図解。
equalsを上書きするときは、実装前に同一性の基準を決めます。
@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (!(obj instanceof ProductCode other)) {
        return false;
    }
    return Objects.equals(this.value, other.value);
}

@Override
public int hashCode() {
    return Objects.hash(value);
}

ここでもequalshashCodeはセットです。比較に使ったvalueと、ハッシュ値に使うvalueを揃えます。

注意点1:比較フィールドを何となく選ばない

IDEやLombokで生成するとき、フィールドを全部選んでしまうことがあります。しかし、業務上の同一性とすべてのフィールドが一致することは別です。

public class User {
    private Long id;
    private String name;
    private String email;
    private LocalDateTime lastLoginAt;
}

このクラスでユーザーを同一判定したい場合、idだけを見るのか、メールアドレスも見るのか、ログイン日時まで見るのかで意味が変わります。lastLoginAtを比較に含めると、ログインするたびに別ユーザー扱いになるような危険があります。

比較フィールドを選ぶ観点

  • 業務上、何が同じなら同じものと言えるか
  • その値は作成後に変わらないか
  • 画面表示用の名前や日時を同一判定に含めてよいか
  • DBの主キー、業務キー、画面入力値のどれを基準にするか

注意点2:変更される値を比較に入れない

SetやMapのキーにするクラスでは、比較に使う値が後から変わると危険です。入れたときと探すときでハッシュ値が変わると、取り出せなくなることがあります。

public class UserKey {
    private String userId;

    public void setUserId(String userId) {
        this.userId = userId;
    }

    @Override
    public int hashCode() {
        return Objects.hash(userId);
    }
}

このようなクラスをMapのキーにするなら、userIdを後から変更できない設計にする方が安全です。

注意点3:Entityのequalsはプロジェクト方針を見る

JPAのEntityでは、equalsの扱いが難しくなります。IDが採番前はnullだったり、遅延ロードやプロキシが絡んだりするためです。

新人のうちは、Entityに対して自分の判断だけでequals/hashCodeを追加しない方が安全です。既存Entityの書き方、チームの規約、レビュー担当者の方針を確認しましょう。

// レビューコメント例
// Entityにequals/hashCodeを追加していますが、プロジェクトではEntityの同一判定をIDベースに統一しています。
// 採番前の扱いもあるため、既存Entityと同じ方針に合わせてください。

注意点4:自動生成しても意味は説明できるようにする

IDE生成やLombokを使えば、コード自体は短くできます。しかし、レビューでは「なぜそのフィールドを比較に使ったのか」を説明できるかが見られます。

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class ProductCode {
    @EqualsAndHashCode.Include
    private final String value;
}

このように明示的に含めるフィールドを指定すると、意図が読み手に伝わりやすくなります。自動生成を使う場合でも、設計判断は人間が行います。

レビュー前のセルフチェック

equalsオーバーライドの確認リスト

  • equalshashCodeをセットで実装しているか
  • 比較フィールドを業務上の同一性で説明できるか
  • 変更される値を比較に入れていないか
  • EntityやDTOでプロジェクト方針に反していないか
  • テストで同じケースと違うケースを確認したか

特に、同じ値なら同じになるテストだけでなく、違う値なら違うになるテストも入れておくと安心です。

ProductCode code1 = new ProductCode("P001");
ProductCode code2 = new ProductCode("P001");
ProductCode code3 = new ProductCode("P002");

assertThat(code1).isEqualTo(code2);
assertThat(code1).isNotEqualTo(code3);

この記事とあわせて読みたい

まとめ

equalsのオーバーライドは、ただの定型コードではありません。何を同じとみなすかを決める設計です。自動生成を使う場合でも、比較フィールド、変更可能性、hashCodeとの整合性を説明できる状態にしてからレビューに出しましょう。

-Java
-, , , ,