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


equalsは、オブジェクト同士を「同じものとして扱うか」を決めるメソッドです。
現場では、ID、コード値、注文番号など、業務上の同一性を表すクラスでよく話題になります。
この記事のポイント
equalsは同一判定のルールを表す- オーバーライドするなら
hashCodeもセットで考える - 比較に使うフィールドは業務上の意味で決める
- 可変フィールドを比較に入れるとSetやMapで事故りやすい
equalsの比較方向で結果が変わる例を再現する
JDK 17を用意し、ZIPを展開したフォルダでjava ProductCodeEqualityDemo.javaを実行します。修正前は親から子がtrue、子から親がfalseです。修正後は同じ値を両方向から比べ、ハッシュ値、別の値、nullも確認します。Javaファイルの全文と手順書を収録しています。
equalsは何を同じとみなすかのルール
初学者のうちは、equalsを「オブジェクトを比較するメソッド」とだけ覚えがちです。しかし実務では、もう一歩踏み込んで「何を同じとみなすか」を決める設計になります。
public class ProductCode {
private final String value;
public ProductCode(String value) {
this.value = Objects.requireNonNull(value);
}
}
ここで作るProductCodeは、文字列で受け取った商品コードを表す値クラスです。コードが同じなら同じ値として扱うため、equalsではvalueを比べます。上は比較を追加する前の宣言です。
まず守るべき基本形
equalsを手書きする場合は、自己比較、型チェック、フィールド比較の順で考えると読みやすくなります。

比較に使う値を子クラスが増やすと、比較する向きで結果が変わることがあります。修正前の親クラスをサンプルではOpenProductCodeと呼び、instanceof OpenProductCodeで子クラスも受け入れます。次の子クラスは、商品コードに加えて地域も一致する場合だけtrueを返します。
static final class RegionalProductCode extends OpenProductCode {
private final String region;
RegionalProductCode(String value, String region) {
super(value);
this.region = Objects.requireNonNull(region);
}
@Override
public boolean equals(Object obj) {
return obj instanceof RegionalProductCode other
&& super.equals(other)
&& region.equals(other.region);
}
}コードがP001の親と、コードがP001・地域がJPの子を比べます。親から子を比べるとコードだけを見るのでtrue、子から親を比べると型が合わずfalseです。比較の向きを逆にしても結果が一致するという、equalsの対称性を守れていません。
この値クラスの役割は商品コードだけを表すことです。継承が要件にないため、finalを付けて子クラスを作れないようにします。以下は配布ファイルの中に置いた修正後のクラスです。入れ子のクラスなのでstaticが付きます。地域も識別に必要なら、商品コードと地域を持つ別の値クラスを設計します。
public static final class ProductCode {
private final String value;
public ProductCode(String value) {
this.value = Objects.requireNonNull(value);
}
@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);
}
}ここでもequalsとhashCodeはセットです。比較に使った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);
}
}
上は、ハッシュ値に使うuserIdを変更できてしまう例です。Mapのキーにするなら、作成時に決めたuserIdを保持し、同じ値を使ってequalsとhashCodeを実装します。
注意点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;
}
上はLombokで比較対象を指定する部分の抜粋です。実際に今回と同じ値クラスにするなら、初期化のためのコンストラクターを用意し、クラスをfinalにするかも設計として判断します。
レビュー前のセルフチェック
equalsオーバーライドの確認リスト
equalsとhashCodeをセットで実装しているか- 比較フィールドを業務上の同一性で説明できるか
- 変更される値を比較に入れていないか
- EntityやDTOでプロジェクト方針に反していないか
- 同じ値と異なる値をそれぞれ両方向から比較したか
配布ファイルは、同じ商品コードの両方向の比較、ハッシュ値、別の商品コード、nullを次のように確認します。checkは結果と期待値が異なるとAssertionErrorを出します。
var first = new ProductCode("P001");
var second = new ProductCode("P001");
var different = new ProductCode("P002");
check("同じコードは両方向で一致", first.equals(second) && second.equals(first), true);
check("同じコードはハッシュ値も同じ", first.hashCode() == second.hashCode(), true);
check("別コードは両方向で不一致", !first.equals(different) && !different.equals(first), true);
check("nullとの比較", first.equals(null), false);この記事とあわせて読みたい
まとめ
equalsのオーバーライドは、ただの定型コードではありません。何を同じとみなすかを決める設計です。自動生成を使う場合でも、比較フィールド、変更可能性、hashCodeとの整合性を説明できる状態にしてからレビューに出しましょう。



