Java Tips | 基本ユーティリティ:hashCode生成

Java Java
スポンサーリンク

hashCode ってそもそも何をしているのか

hashCode() は、オブジェクトから「ハッシュ値」と呼ばれる整数を計算するためのメソッドです。
この整数は、そのオブジェクトの「ざっくりした特徴」を表す指紋のようなもので、HashMapHashSet などのコレクションが内部で高速に検索するために使われます。

イメージとしては、「本棚のどの棚に本を置くかを決める番号」です。
hashCode() が同じ本は同じ棚に集められ、その棚の中で equals() を使って「本当に同じ本かどうか」を確認します。
だからこそ、equals()hashCode() はセットで考えないといけないし、「どうやって hashCode を作るか」が業務コードでも重要になります。


equals と hashCode の「約束」をまず理解する

equals が同じなら hashCode も同じでなければならない

Java には、equalshashCode の間に有名な約束があります。
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 だけから計算しています。
もし equalsid だけを見ているのに、hashCodename まで混ぜてしまうと、「equals では同じなのに hashCode が違う」という矛盾が起きてしまいます。

HashSetHashMap は、「同じハッシュ値のものだけを 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);
    }
}
Java

Objects.hash(id, name, age) は、内部でそれぞれのフィールドの hashCode() を呼び出し、適当に混ぜ合わせて 1 つの int 値にしてくれます。
null にも対応していて、フィールドが null の場合は 0 として扱ってくれるので、null チェックを自分で書く必要がありません。

初心者のうちは、「equals で比較に使っているフィールドを、そのまま Objects.hash に渡す」と覚えておけば十分です。
これだけで、equalshashCode の約束を守った、そこそこバランスの良いハッシュコードが手に入ります。

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 生成コードの良さ

実務では、equalshashCode を毎回手で書くのは正直しんどいです。
そこで、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

このように、「equalshashCode で同じフィールドを使う」「Objects.equalsObjects.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 のキーにする可能性があるか?」と自問して、「あり得る」と思ったら、equalshashCode をセットで実装する、という癖をつけておくと安全です。

DTO や一時的なデータではそこまで神経質にならなくていいこともある

一方で、画面とのやり取りにだけ使う DTO や、一時的なデータを運ぶだけのクラスなど、HashMap のキーにも HashSet の要素にもならないクラスでは、equals/hashCode を実装しないこともあります。

ただし、「後から誰かがそのクラスをキーに使い始める」こともあるので、
「ID を持っていて、同一性の概念があるクラス」は、最初から equals/hashCode を実装しておくほうが無難です。


まとめ:初心者が hashCode 生成で身につけるべき感覚

hashCode 生成で一番大事なのは、「equals とセットで考える」という感覚です。
「何を同じとみなすか」をまず決め、そのフィールドだけを使って equalshashCode を実装する。
実装方法としては、まずは Objects.hash を使うシンプルな形から入り、必要に応じて手書きのパターンや IDE 生成を使い分ける。

ここまで押さえれば、「hashCode をどう書けばいいか分からない」という状態からは卒業できます。

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