導入:なぜequalsとhashCodeの両方が必要なのか
Javaで開発をしていると、オブジェクトの比較を行う際に必ず直面するのがequalsメソッドとhashCodeメソッドのオーバーライドです。なぜこの二つはセットで語られるのでしょうか。それは、HashMapやHashSetといった「ハッシュ系コレクション」を正しく動作させるために、この二つのメソッドが論理的な整合性(契約)を保つ必要があるからです。この契約を破ると、コレクションに入れたはずのデータが見つからないという、デバッグが極めて困難なバグを引き起こします。
基礎知識:二つのメソッドの役割
equalsメソッドは、二つのオブジェクトが「論理的に等しいか」を判定するために使います。デフォルトのObjectクラスの実装は「メモリ上の同一アドレスか(==)」を判定しますが、ビジネスロジック上は「IDが同じなら等しい」と定義したい場合が多いでしょう。
hashCodeメソッドは、そのオブジェクトをハッシュテーブルのどこに格納するかを決めるための「整数値」を返します。ハッシュ系コレクションは、まずhashCodeで大まかな格納場所を特定し、その場所で衝突が発生した場合にのみequalsで厳密な比較を行います。つまり、「等しいと判定されるオブジェクトは、必ず同じハッシュコードを持たなければならない」というルールが不可欠なのです。
実装:契約を守るためのルール
equalsをオーバーライドする際は、以下の原則を守る必要があります。
1. 再帰性・対称性・推移性を満たすこと。
2. instanceof演算子を活用し、型チェックを確実に行うこと。
3. hashCodeも同時にオーバーライドすること。
4. equalsで比較に使用したフィールドのみを、hashCodeの計算にも使用すること。
サンプルプログラム:安全な実装例
以下は、Java 16以降で導入された「instanceofによるパターンマッチング」を活用した、現代的で安全な実装例です。
import java.util.Objects;
public class User {
private final String id;
private final String name;
public User(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public boolean equals(Object o) {
// 自分自身との比較ならtrue
if (this == o) return true;
// instanceofパターンマッチングで型チェックとキャストを同時に行う
if (!(o instanceof User user)) return false;
// IDと名前が等しいかを比較
return Objects.equals(id, user.id) &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
// equalsで使用したフィールドのみを計算対象にする
return Objects.hash(id, name);
}
}
応用・注意点:現場での落とし穴
現場で最も多いトラブルは、「hashCodeをオーバーライドし忘れる」こと、あるいは「equalsでは使っていないフィールドをhashCodeに含めてしまう」ことです。
また、もしJava 14以降のプロジェクトであれば、可能な限り record型 を使用することを強く推奨します。recordは、equals、hashCode、toStringをコンパイラが自動生成してくれるため、今回解説したような「契約違反」を根本から防ぐことができます。
最後に、比較対象のフィールドが変更可能(ミュータブル)な場合、hashCodeの計算に使っているフィールドを書き換えると、コレクション内での検索ができなくなるという致命的な問題が発生します。可能な限り、equalsやhashCodeには不変(Immutable)なフィールドを使用するように設計してください。これがシニアエンジニアとしての「堅牢なコード」への第一歩です。

コメント