【プログラミングしてみよう48】JavaのequalsとhashCodeを正しくオーバーライドする

Java

前回はComparableを使って自作クラスに自然順序を定義する方法を学びました。

今回は「equals」と「hashCode」について解説します。 自作クラスのオブジェクトを ==equals() で比較したとき、期待通りに動かない経験はないでしょうか。正しく動かすためには equalshashCode をセットでオーバーライドする必要があります。

equalsとhashCodeとは

Javaのすべてのクラスは Object クラスを継承しており、equals()hashCode() はそこで定義されています。

デフォルトの equals() はオブジェクトの参照(メモリアドレス)が同じかどうかを比較します。つまり new で作った2つのオブジェクトは、中身が同じでも equals()false になります。

Person p1 = new Person("ぱんまる", 25);
Person p2 = new Person("ぱんまる", 25);
System.out.println(p1 == p2);       // false(別のオブジェクト)
System.out.println(p1.equals(p2));  // false(デフォルトは参照比較)

hashCode() はオブジェクトを整数値(ハッシュ値)に変換するメソッドで、HashMapHashSet がオブジェクトを格納・検索するときに使います。

Javaには重要なルールがあります。equals()true を返す2つのオブジェクトは、必ず同じ hashCode() を返さなければなりません。 このため、equals をオーバーライドしたら hashCode も必ずセットでオーバーライドする必要があります。

サンプルコード(オーバーライドしない場合の問題)

ファイル作成

「Java36」フォルダに EqualsTest.java という名前でファイルを作成します

コード入力

以下のコードを入力します。

import java.util.HashSet;
import java.util.HashMap;
import java.util.Set;
import java.util.Map;

public class EqualsTest {
    // equalsもhashCodeもオーバーライドしていないクラス
    static class Point {
        int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
    }

    public static void main(String[] args) {
        Point p1 = new Point(3, 5);
        Point p2 = new Point(3, 5);

        // ==はオブジェクトの参照を比較
        System.out.println("p1 == p2: " + (p1 == p2));

        // equalsのデフォルトも参照比較
        System.out.println("p1.equals(p2): " + p1.equals(p2));

        // HashSetで重複排除できない
        Set<Point> set = new HashSet<>();
        set.add(p1);
        set.add(p2);
        System.out.println("SetのサイズP(期待値1): " + set.size());

        // HashMapで検索できない
        Map<Point, String> map = new HashMap<>();
        map.put(p1, "原点付近");
        System.out.println("map.get(p2): " + map.get(p2)); // nullになってしまう
    }
}

コンパイル

javac EqualsTest.java

実行

java EqualsTest

実行結果は次のようになります。

p1 == p2: false
p1.equals(p2): false
SetのサイズP(期待値1): 2
map.get(p2): null

サンプルコード(equalsとhashCodeを正しく実装する)

ファイル作成

「Java36」フォルダに PointFixed.javaEqualsFixedTest.java という名前でファイルを作成します

コード入力

PointFixed.java に以下のコードを入力します。

import java.util.Objects;

public class PointFixed {
    int x, y;

    PointFixed(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public boolean equals(Object obj) {
        // 同じ参照なら true
        if (this == obj) return true;
        // nullまたは異なるクラスなら false
        if (obj == null || getClass() != obj.getClass()) return false;
        // フィールドの値で比較
        PointFixed other = (PointFixed) obj;
        return this.x == other.x && this.y == other.y;
    }

    @Override
    public int hashCode() {
        // equalsで使うフィールドを使ってハッシュ値を生成
        return Objects.hash(x, y);
    }

    @Override
    public String toString() {
        return "Point(" + x + ", " + y + ")";
    }
}

EqualsFixedTest.java に以下のコードを入力します。

import java.util.HashSet;
import java.util.HashMap;
import java.util.Set;
import java.util.Map;

public class EqualsFixedTest {
    public static void main(String[] args) {
        PointFixed p1 = new PointFixed(3, 5);
        PointFixed p2 = new PointFixed(3, 5);
        PointFixed p3 = new PointFixed(1, 2);

        // equals が内容で比較されるようになった
        System.out.println("p1 == p2: " + (p1 == p2));
        System.out.println("p1.equals(p2): " + p1.equals(p2));
        System.out.println("p1.equals(p3): " + p1.equals(p3));

        // hashCodeも同じ値になっている
        System.out.println("p1.hashCode(): " + p1.hashCode());
        System.out.println("p2.hashCode(): " + p2.hashCode());

        // HashSetで正しく重複排除できる
        Set<PointFixed> set = new HashSet<>();
        set.add(p1);
        set.add(p2);
        set.add(p3);
        System.out.println("\nSetのサイズ(期待値2): " + set.size());
        System.out.println("Set: " + set);

        // HashMapで正しく検索できる
        Map<PointFixed, String> map = new HashMap<>();
        map.put(p1, "原点付近");
        System.out.println("\nmap.get(p2): " + map.get(p2)); // p2でもp1の値が取得できる
        System.out.println("map.get(p3): " + map.get(p3));
    }
}

コンパイル

javac PointFixed.java EqualsFixedTest.java

実行

java EqualsFixedTest

実行結果は次のようになります。

p1 == p2: false
p1.equals(p2): true
p1.equals(p3): false
p1.hashCode(): 1059
p2.hashCode(): 1059

Setのサイズ(期待値2): 2
Set: [Point(1, 2), Point(3, 5)]

map.get(p2): 原点付近
map.get(p3): null

コードの解説

equals() の実装では、まず this == obj で同じ参照かどうかを確認します(パフォーマンス最適化)。次に obj == null || getClass() != obj.getClass() でnullチェックと型チェックを行います。型が同じであればキャストして各フィールドの値を比較します。

hashCode() には Objects.hash(x, y) を使っています。Objects.hash() は複数のフィールドをまとめてハッシュ値に変換してくれる便利なメソッドで、equals() で使っているフィールドをすべて渡すのが正しい実装です。

以前学んだ recordequals()hashCode() を自動生成するため、この実装を書く必要がありません。自作の通常クラスで内容による比較が必要な場合に今回の実装が必要になります。

HashSetHashMap はオブジェクトを格納するときにまず hashCode() でバケット(格納場所)を決め、同じバケットのオブジェクトと equals() で比較します。そのため hashCode() が異なると同じバケットに入らず、equals() が呼ばれないまま別オブジェクトとして扱われてしまいます。これが equalshashCode をセットで実装しなければならない理由です。

まとめ

自作クラスで内容による比較が必要な場合は、equals()hashCode() をセットでオーバーライドする必要があります。equals() では参照チェック・nullチェック・型チェック・フィールド比較の順で実装し、hashCode() では Objects.hash() を使って equals() で使ったフィールドをすべて渡します。HashSetHashMap を正しく動かすためにもこの2つの実装は欠かせません。record を使えば自動生成されるため、データ保持クラスには record を優先して使うと手間が省けます。

2026/07/27(月) ぱんまる

コメント

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