Java | オブジェクト指向:hashCode のオーバーライド

Java Java
スポンサーリンク

なぜ 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 で安心して使える堅牢な値判定が完成します。

タイトルとURLをコピーしました