Java | 基礎文法:hashCode の基礎

Java Java
スポンサーリンク

hashCode の全体像

hashCode は「オブジェクトを整数に写像する関数」で、主に HashMap/HashSet が高速に探索するために使います。equals が「意味として同じか」を判定するのに対し、hashCode は「同じかもしれない候補」を素早く絞り込む合図です。設計の核心は「equals と整合すること」「広く均一に散ること」「変更で壊れないこと」。ここを押さえると、コレクションが意図どおりに動き、性能も安定します。


equals と hashCode の契約(最重要ポイント)

契約の要点

  • 同値なら同じハッシュ: x.equals(y) が true のとき、x.hashCode() == y.hashCode() でなければならない。
  • 不変で一貫: 同じ実行中、オブジェクトの「同値性を決めるフィールド」が変わらない限り、返す値は変わらない。
  • 異なるなら必ず異なる必要はない: 異なるオブジェクトが同じハッシュ(衝突)でも契約違反ではない。ただし、衝突が多いと性能が落ちる。

この契約が破られると、HashSet に入れた要素が見つからない、HashMap から取り出せないなど「壊れ方」が発生します。equals をオーバーライドしたら、hashCode も必ず整合する形でオーバーライドします。


仕組みの理解:ハッシュ構造で何が起きているか

バケット選択と同値判定

HashMap/HashSet は、hashCode を使って「バケット(箱)」を選び、そこに入っている要素と equals で最終的な一致を確認します。よく散るハッシュはバケットの偏りを減らし、検索がほぼ O(1) に近づきます。散りが悪い(多衝突)と、バケット内で線形探索や木探索が増え、性能が落ちます。

可変フィールドの危険性

「hashCode に使っているフィールド」を後から変更すると、要素が本来のバケットから“迷子”になります。集合に入れた後にキーを変える設計は避け、比較キーは不変にするのが基本です。


実装の型:正しい hashCode を書く

Objects.hash を使う(簡単・安全)

equals と同じキーでハッシュを作り、フィールドが増減しても楽に保守できます。

import java.util.Objects;

public final class User {
    private final String id;
    private final String name;

    public User(String id, String name) {
        this.id = Objects.requireNonNull(id);
        this.name = Objects.requireNonNull(name);
    }

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User u)) return false; // Java 16+ パターンマッチ
        return Objects.equals(this.id, u.id);
    }

    @Override public int hashCode() {
        return Objects.hash(id); // equals と同じキー(id)だけを使う
    }
}
Java

31 乗算で組み立てる(定番・高速)

プリミティブや軽量クラスで、コストや割り当てを抑えたい場合に有効です。

public final class Point {
    private final int x, y;

    @Override public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Point p)) return false;
        return this.x == p.x && this.y == p.y;
    }

    @Override public int hashCode() {
        int h = 17;           // どこかの奇数から開始
        h = 31 * h + x;       // 31 で掛けながら混ぜる
        h = 31 * h + y;
        return h;             // 32bit のオーバーフローは許容(JVM が前提にしている)
    }
}
Java

整数のオーバーフローは問題ありません。ハッシュ値は「均一に散る」ことが目的であり、数学的な厳密性は不要です。

配列・ネスト構造は Arrays.hashCode/deepHashCode

配列の hashCode は参照のデフォルトを返すため、内容でハッシュしたいなら Arrays.hashCode/deepHashCode を使います。

import java.util.Arrays;

int[] xs = {1,2,3};
int h = Arrays.hashCode(xs); // 内容に基づくハッシュ
Java

record は自動生成に乗る

Java の record は equals/hashCode/toString が「全成分での同値性」に整合するよう自動生成されます。値オブジェクトなら record を選ぶと安全です。

record Money(int amount, String currency) {} // 生成済みの equals/hashCode が整合
Java

よくある落とし穴(重要ポイントの深掘り)

equals と hashCode のキー不一致

equals は id 比較、hashCode は name を混ぜる、のような不一致は厳禁です。セットやマップで「入れたのに取れない」状態が発生します。equals と hashCode は「同じキー」で作るのが鉄則です。

可変キーを変えてしまう

集合に入れた後、equals/hashCode のキー(id など)を変更すると、検索しても見つからなくなります。キーは不変(final)にするか、ミュータブルならコレクション投入後に変更しない規約を設けます。

var set = new java.util.HashSet<Point>();
var p = new Point(1, 2);
set.add(p);
// p.setX(9); // こういう変更がある設計は避ける(不変にする)
Java

BigDecimal と比較の方針

BigDecimal.equals はスケールも比較します(1.0 と 1 は equals false)。「数値として同じ」をキーにしたいなら compareTo == 0 相当の正規化(stripTrailingZeros など)を設計に入れ、hashCode もその方針に合わせます。

var a = new java.math.BigDecimal("1.0").stripTrailingZeros();
var b = new java.math.BigDecimal("1").stripTrailingZeros();
System.out.println(a.equals(b));     // true
System.out.println(a.hashCode() == b.hashCode()); // true(同じ表現へ正規化)
Java

コレクションのハッシュと順序

List/Set/MaphashCode は内容に依存します。Set は要素のハッシュを合算、List は順序もハッシュに影響します。順序を変える可能性があるなら、比較・ハッシュ方針も「順序を含む/含まない」を意図的に決めましょう。


例題で身につける

例 1: HashSet で正しく重複判定

import java.util.*;

public class Demo1 {
    public static void main(String[] args) {
        var s = new HashSet<User>();
        s.add(new User("A", "Sato"));
        s.add(new User("A", "Suzuki")); // id 同一→ equals true → 追加されない(重複)
        System.out.println(s.size());   // 1
        System.out.println(s.contains(new User("A", "Yamada"))); // true(hashCode 整合で見つかる)
    }
}
Java

equals/hashCode を同じキー(id)に揃えているため、重複排除も検索も期待通りに動きます。

例 2: 不整合で壊れる例(やってはいけない)

public final class BadUser {
    private final String id;
    private final String name;

    @Override public boolean equals(Object o) {
        if (!(o instanceof BadUser u)) return false;
        return java.util.Objects.equals(this.id, u.id); // id だけで比較
    }
    @Override public int hashCode() {
        return java.util.Objects.hash(name);            // name でハッシュ(不一致)
    }
}
Java

このクラスを HashSet に入れると「equals は一致するのに、別バケットへ行く」ため、contains が false を返すなどの壊れ方が起こり得ます。

例 3: 31 乗算の軽量実装

final class Pair {
    final int a;
    final int b;
    Pair(int a, int b) { this.a = a; this.b = b; }

    @Override public boolean equals(Object o) {
        if (!(o instanceof Pair p)) return false;
        return a == p.a && b == p.b;
    }
    @Override public int hashCode() {
        int h = 17;
        h = 31 * h + a;
        h = 31 * h + b;
        return h;
    }
}
Java

この形は JDK でも多用される定石で、速くて分布も良好です。


設計の指針と仕上げのアドバイス

指針

  • 整合性: equals と hashCode は同じキーで作る(片方だけの変更はしない)。
  • 不変性: キーとなるフィールドは不変化(final)するか、投入後に変更しない。
  • 分布: 多様な入力で偏りなく散るように、複数フィールドを混ぜ、31 乗算や Objects.hash を使う。
  • 配列・ネスト: Arrays.hashCode/deepHashCode を用いて「内容」でハッシュする。
  • 特殊値: BigDecimal/浮動小数点は方針を明示(スケールを揃える、許容誤差はハッシュに使わない)。

まとめ

hashCode は「速さのための近道」ですが、同値性(equals)という土台なしには成立しません。同値なら同じハッシュ、不変で一貫、よく散る——この三点を満たせば、HashMap/HashSet はあなたの意図どおりに正しく速く動きます。迷ったクラスやキー選定があれば見せてください。equals/hashCode の整合、分布の改善、可変性の扱いまで、最短で壊れない設計に整えます。

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