hashCode ってそもそも何をしているのか
hashCode() は、オブジェクトから「ハッシュ値」と呼ばれる整数を計算するためのメソッドです。
この整数は、そのオブジェクトの「ざっくりした特徴」を表す指紋のようなもので、HashMap や HashSet などのコレクションが内部で高速に検索するために使われます。
イメージとしては、「本棚のどの棚に本を置くかを決める番号」です。hashCode() が同じ本は同じ棚に集められ、その棚の中で equals() を使って「本当に同じ本かどうか」を確認します。
だからこそ、equals() と hashCode() はセットで考えないといけないし、「どうやって hashCode を作るか」が業務コードでも重要になります。
equals と hashCode の「約束」をまず理解する
equals が同じなら hashCode も同じでなければならない
Java には、equals と hashCode の間に有名な約束があります。
「equals で等しいと判定される 2 つのオブジェクトは、必ず同じ hashCode を返さなければならない」というものです。
例えば、次のような User クラスを考えます。
public class User {
private final String id;
private final String name;
// コンストラクタなどは省略
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(id, other.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
Javaこのクラスでは、「id が同じなら同じユーザー」とみなしています。
だから hashCode() も id だけから計算しています。
もし equals は id だけを見ているのに、hashCode で name まで混ぜてしまうと、「equals では同じなのに hashCode が違う」という矛盾が起きてしまいます。
HashSet や HashMap は、「同じハッシュ値のものだけを equals で詳しく比べる」という前提で動いているので、この約束を破ると「入れたはずの要素が見つからない」「同じキーを入れたのに上書きされない」といった不可解なバグになります。
hashCode が同じでも equals が同じとは限らない
逆に、「hashCode が同じなら必ず equals も同じでなければならない」という約束はありません。
ハッシュ値は有限の整数なので、違うオブジェクトがたまたま同じハッシュ値になること(ハッシュ衝突)は普通に起こり得ます。
つまり、「equals が同じなら hashCode も同じ」「hashCode が同じでも equals は違うことがある」という関係です。
この前提を押さえたうえで、「どうやって hashCode を作るか」を考えていきます。
Objects.hash を使ったシンプルな hashCode 生成
Objects.hash の基本的な使い方
Java 7 以降には、java.util.Objects.hash という便利メソッドがあります。
これは、渡したフィールドからそれっぽいハッシュ値をまとめて計算してくれるユーティリティです。
import java.util.Objects;
public class User {
private final String id;
private final String name;
private final int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(id, other.id)
&& Objects.equals(name, other.name)
&& age == other.age;
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
}
JavaObjects.hash(id, name, age) は、内部でそれぞれのフィールドの hashCode() を呼び出し、適当に混ぜ合わせて 1 つの int 値にしてくれます。null にも対応していて、フィールドが null の場合は 0 として扱ってくれるので、null チェックを自分で書く必要がありません。
初心者のうちは、「equals で比較に使っているフィールドを、そのまま Objects.hash に渡す」と覚えておけば十分です。
これだけで、equals と hashCode の約束を守った、そこそこバランスの良いハッシュコードが手に入ります。
Objects.hash のメリットとデメリット
メリットは、とにかくコードが短くて読みやすいことです。null 対応もしてくれるので、「id == null ? 0 : id.hashCode()」のようなボイラープレートを書かなくて済みます。
一方で、デメリットとしては「パフォーマンスがそこまで良くない」ことが挙げられます。
内部で可変長引数配列を作ったりするので、超大量のオブジェクトをハッシュベースのコレクションに詰め込むような場面では、手書きの hashCode より遅くなることがあります。
ただし、業務システムの普通の規模であれば、まず気にしなくていいレベルです。
「まずは Objects.hash で書いておき、どうしてもパフォーマンスが問題になったら手書きに切り替える」というスタンスで十分です。
手書きで hashCode を作るときの考え方
典型的なパターンと「31 の魔法」
昔からよく使われている典型的な hashCode 実装パターンがあります。
public class User {
private final String id;
private final String name;
private final int age;
@Override
public int hashCode() {
int result = 17;
result = 31 * result + (id == null ? 0 : id.hashCode());
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + age;
return result;
}
}
Javaここで出てくる「17」と「31」は、よく使われる「適当な奇数の素数」です。
なぜ 31 なのかというと、2 の累乗に近くなくて、ビット演算的にも都合が良くて、昔から慣習的に使われているから、くらいの理解で大丈夫です。
重要なのは、「前の結果に 31 を掛けてから次のフィールドのハッシュ値を足す」というパターンです。
これによって、フィールドの順番や値の違いが、ハッシュ値にそれなりに反映されるようになります。
null をどう扱うか
手書きで hashCode を書くときに必ず出てくるのが、「フィールドが null のときどうするか」です。
典型的には、「null のときは 0、それ以外はそのフィールドの hashCode」という扱いにします。
int idHash = (id == null) ? 0 : id.hashCode();
Javaこうしておけば、「id が null のオブジェクトどうし」は、equals の実装次第では同じとみなされるので、そのときに同じハッシュ値になるようにできます。Objects.hash も内部的には同じような扱いをしています。
equals と hashCode を IDE に生成させる、という現実的な選択
IDE 生成コードの良さ
実務では、equals と hashCode を毎回手で書くのは正直しんどいです。
そこで、IntelliJ IDEA や Eclipse などの IDE には、「フィールドを選ぶと equals/hashCode を自動生成してくれる」機能があります。
生成されるコードは、だいたい次のようなパターンです。
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return age == user.age &&
Objects.equals(id, user.id) &&
Objects.equals(name, user.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name, age);
}
Javaこのように、「equals と hashCode で同じフィールドを使う」「Objects.equals と Objects.hash を使う」という、教科書的な実装を一瞬で作ってくれます。
初心者のうちは、まず IDE に生成させて、そのコードを読みながら「何をしているのか」を理解していくのが一番現実的で安全です。
生成に頼りきりにしないために
ただし、「IDE が作ってくれたから OK」と思考停止してしまうのは危険です。
特に業務では、「どのフィールドで同一性を判断するか」がドメインの設計そのものに関わってきます。
ID だけで同一とみなすのか。
名前や生年月日も含めて同一とみなすのか。
論理削除フラグは同一性に含めるのか。
こういった設計を自分で考えたうえで、「その設計に合うように equals/hashCode を生成する」という順番が大事です。
IDE はあくまで「手を動かす部分を代わりにやってくれるツール」であって、「何を同じとみなすか」を決めてくれるわけではありません。
hashCode を意識すべき場面と、あまり気にしなくていい場面
HashMap / HashSet のキーや要素に使うとき
hashCode を真面目に考えないと痛い目を見るのは、主に「ハッシュベースのコレクションに入れるとき」です。
具体的には、HashMap のキーや、HashSet の要素として自作クラスを使う場合です。
Set<User> users = new HashSet<>();
users.add(new User("u001", "Taro"));
users.add(new User("u001", "Taro")); // equals/hashCode 次第で重複とみなされるかが決まる
Javaここで equals だけ実装して hashCode を実装しないと、「同じユーザーを 2 回 add しても重複とみなされない」といったバグになります。
逆に、hashCode だけ実装して equals を実装しないのも同様に危険です。
「このクラスを HashMap のキーにする可能性があるか?」と自問して、「あり得る」と思ったら、equals と hashCode をセットで実装する、という癖をつけておくと安全です。
DTO や一時的なデータではそこまで神経質にならなくていいこともある
一方で、画面とのやり取りにだけ使う DTO や、一時的なデータを運ぶだけのクラスなど、HashMap のキーにも HashSet の要素にもならないクラスでは、equals/hashCode を実装しないこともあります。
ただし、「後から誰かがそのクラスをキーに使い始める」こともあるので、
「ID を持っていて、同一性の概念があるクラス」は、最初から equals/hashCode を実装しておくほうが無難です。
まとめ:初心者が hashCode 生成で身につけるべき感覚
hashCode 生成で一番大事なのは、「equals とセットで考える」という感覚です。
「何を同じとみなすか」をまず決め、そのフィールドだけを使って equals と hashCode を実装する。
実装方法としては、まずは Objects.hash を使うシンプルな形から入り、必要に応じて手書きのパターンや IDE 生成を使い分ける。
ここまで押さえれば、「hashCode をどう書けばいいか分からない」という状態からは卒業できます。
