なぜ hashCode をオーバーライドするのか
hashCode は「オブジェクトのハッシュ値」を返すメソッドで、HashMap・HashSet などのハッシュ構造が「どのバケットに入れるか」を決めるために使います。equals を値ベースでオーバーライドしたら、必ず同じ基準で hashCode もオーバーライドが必要です。理由は「equals で等しいものは、常に同じハッシュ値でなければならない」からです。これを破ると、セットに重複が入ったり、マップから取り出せなくなるといった不具合が起きます。
契約と前提(重要)
何を守るべきか
- 等価の整合: equals が true の2つのオブジェクトは、同じ hashCode を返さなければならない。
- 一貫性: 同じ実行中でオブジェクトの等価に使うフィールドが変わらない限り、hashCode は何度呼んでも同じ値を返すべき。
- 衝突はあり得る: equals が false の2つが同じ hashCode を返すこと自体は許されるが、なるべく避けると性能が良くなる。
この契約を満たすために、equals の比較に使った「同じフィールド集合」で hashCode を計算するのが鉄則です。可変フィールドをハッシュ計算に使うと、コレクション格納後にハッシュ値が変わり、取り出せなくなるバグが起きるので避けます。
基本の実装パターン
値オブジェクトの定型(Objects.hash を使う)
比較に使うフィールドを並べるだけで安全にハッシュを作れます。読みやすく、メンテが容易です。
import java.util.Objects;
public final class Money {
private final int amount;
private final String currency;
public Money(int amount, String currency) {
if (amount < 0) throw new IllegalArgumentException("amount>=0");
if (currency == null || currency.isBlank()) throw new IllegalArgumentException("currency");
this.amount = amount;
this.currency = currency.trim();
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money other = (Money) o;
return amount == other.amount && currency.equals(other.currency);
}
@Override public int hashCode() {
return Objects.hash(amount, currency); // equals と同じフィールド
}
}
Java手書き計算(軽量・高速寄り)
プリミティブや短い文字列なら、乗算(31 が定番)と加算の組み合わせで手書きすると軽量です。
public final class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point other = (Point) o;
return x == other.x && y == other.y;
}
@Override public int hashCode() {
int h = 17;
h = 31 * h + x;
h = 31 * h + y;
return h;
}
}
Java可変の危険性と対策(重要ポイントの深掘り)
可変フィールドを使うと何が起きるか
格納後に値を変えると、ハッシュが変わって「同じバケットにいない」ため見つからなくなります。これが最も多いバグです。
import java.util.*;
public final class Product {
private String code; // equals/hashCode に使う(可変は危険)
public Product(String code) { this.code = code; }
public void changeCode(String c) { this.code = c; }
@Override public boolean equals(Object o) {
return o instanceof Product p && Objects.equals(code, p.code);
}
@Override public int hashCode() { return Objects.hash(code); }
public static void main(String[] args) {
var set = new HashSet<Product>();
var p = new Product("A");
set.add(p);
System.out.println(set.contains(new Product("A"))); // true
p.changeCode("B"); // 格納後に変更
System.out.println(set.contains(new Product("A"))); // false(見つからない)
}
}
Java対策は「等価に使うフィールドは不変(final)にする」か「コレクション格納後に変わらない運用にする」こと。値オブジェクトは原則すべて不変にするのが安全です。
コレクション・配列の扱い
配列や可変コレクションをハッシュに使うなら、内容に基づくハッシュ(Arrays.hashCode / List の要素ハッシュ)にし、受け取り時・返却時に防御的コピーで不変を保ちます。
実務指針と設計のコツ
equals と同じフィールドを使う
基準がズレると契約違反になります。equals に使ったフィールド集合を、そのまま hashCode にも使うのが唯一の正解です。
不変に寄せる(final クラス+final フィールド)
不変なら hashCode の一貫性が自然に守られます。計算コストが高い場合は「初回計算をキャッシュ」しても良い(スレッドセーフに注意)。
public final class Email {
private final String value;
private final int hash; // キャッシュ
public Email(String raw) {
var v = raw == null ? "" : raw.trim().toLowerCase(java.util.Locale.ROOT);
if (!v.contains("@")) throw new IllegalArgumentException("invalid");
this.value = v;
this.hash = v.hashCode(); // 不変なので先に計算
}
@Override public boolean equals(Object o) {
return o instanceof Email e && value.equals(e.value);
}
@Override public int hashCode() { return hash; }
}
Java継承を避けるか、ルールを厳密に共有
値オブジェクトは原則 final にして継承しない方が無難です。継承階層で equals/hashCode の基準が食い違うと、対称律や契約が破綻しやすくなります。
例題で理解を固める
例 1: HashSet/HashMap で期待どおり動くことを確認
var m1 = new Money(100, "JPY");
var m2 = new Money(100, "JPY");
var set = new java.util.HashSet<Money>();
set.add(m1);
System.out.println(set.contains(m2)); // true(値が同じ=ハッシュも同じ)
var map = new java.util.HashMap<Money, String>();
map.put(m1, "ok");
System.out.println(map.get(m2)); // ok(同値キーで取得できる)
Java例 2: Objects.hash と手書きの差
Objects.hash は簡潔で読みやすい一方、ボクシングが入ることがありわずかに遅い場面もあります。性能がクリティカルなら手書き(31 乗算)を検討しますが、まずは可読性重視で十分です。
よくあるつまずきと回避
- equals を作ったのに hashCode を忘れて、セットやマップで同値判定が効かない。必ずセットで実装する。
- 可変フィールドをハッシュに使って、格納後に変更してしまう。不変に寄せるか、等価に使うフィールドは変更禁止に。
- equals と hashCode の基準がズレる。equals に使ったフィールドと同じものだけで hashCode を計算する。
- 配列やコレクションを参照同一性でハッシュ化してしまう。内容ベース(Arrays.hashCode、要素のハッシュ合成)にする。
仕上げのアドバイス(重要部分のまとめ)
hashCode のオーバーライドは「ハッシュ構造で正しく扱われるための契約の締結」です。equals と同じ基準で、同じフィールド集合を使って計算し、一貫性を保つ。不変に寄せれば安全性と性能が両立し、必要なら初回計算をキャッシュする。可変・継承の落とし穴を避ければ、HashMap/HashSet で安心して使える堅牢な値判定が完成します。
