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)だけを使う
}
}
Java31 乗算で組み立てる(定番・高速)
プリミティブや軽量クラスで、コストや割り当てを抑えたい場合に有効です。
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); // 内容に基づくハッシュ
Javarecord は自動生成に乗る
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); // こういう変更がある設計は避ける(不変にする)
JavaBigDecimal と比較の方針
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/Map の hashCode は内容に依存します。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 整合で見つかる)
}
}
Javaequals/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 の整合、分布の改善、可変性の扱いまで、最短で壊れない設計に整えます。
