0Pricing
Java Academy · レッスン

equals/hashCode の契約

両者が一致すべき理由を学びます。

「equals/hashCode の契約」はCoddyKit上の無料Java Academyレッスンです。 これはレッスン2/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはJava Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Java Academyコースには全4レッスンが含まれています。

2つのメソッド、1つの契約

すべての Java オブジェクトは、Object から equals() と hashCode() を継承します。

どちらか一方をオーバーライドした場合、ほぼ必ずもう一方もオーバーライドする必要があります。これらは、ハッシュベースのコレクションが依存する契約を形成します。

public class Main {
    public static void main(String[] args) {
        Object a = new Object();
        Object b = new Object();
        System.out.println(a.equals(b));
        System.out.println(a.hashCode() == b.hashCode());
    }
}

基本ルール

契約では次のように定められています:

  • a.equals(b) が true の場合、a.hashCode() は b.hashCode() と等しくなければなりません。
  • ハッシュコードが異なる場合、オブジェクトが等しくないことが保証されます。

等しいオブジェクトは、等しいハッシュコードを持たなければなりません。逆は必須ではありません。

public class Main {
    public static void main(String[] args) {
        String s1 = "hello";
        String s2 = "hel" + "lo";
        System.out.println(s1.equals(s2));
        System.out.println(s1.hashCode() == s2.hashCode());
    }
}

等しくないオブジェクトが同じハッシュを持つ場合

等しくない2つのオブジェクトが同じハッシュコードを持つことは許容されます。これは単なる衝突であり、マップは equals() を使って処理します。

つまり、唯一禁止されているのは、等しいオブジェクトが異なるハッシュコードを持つことです。

public class Main {
    public static void main(String[] args) {
        // Equal value, unequal objects, same hash is fine
        System.out.println("FB".hashCode() == "Ea".hashCode());
        System.out.println("FB".equals("Ea"));
    }
}

equals だけをオーバーライドするクラス

これは壊れたクラスです。equals() はオーバーライドしていますが、hashCode() はオーバーライドしていません。

その結果、論理的に等しい2つの点が Object から継承した異なるハッシュコードを持ち、契約に違反します。

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        // BUG: no hashCode override
    }
    public static void main(String[] args) {
        Point a = new Point(1, 2);
        Point b = new Point(1, 2);
        System.out.println("equals: " + a.equals(b));
        System.out.println("same hash: " + (a.hashCode() == b.hashCode()));
    }
}

HashSet のバグ

この壊れたクラスは HashSet で正しく動作しません。セットは誤ったハッシュによって選ばれたバケットを検索するため、等しい要素を見つけられません。

その結果、本来なら拒否されるはずの重複が残ってしまいます。

import java.util.HashSet;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

修正方法

両方のメソッドをオーバーライドし、同じフィールドをもとに実装してください。そうすれば、等しいオブジェクトは同じハッシュを持ち、同じバケットに格納されます。

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class Main {
    static class Point {
        final int x, y;
        Point(int x, int y) { this.x = x; this.y = y; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Point p)) return false;
            return x == p.x && y == p.y;
        }
        @Override public int hashCode() { return Objects.hash(x, y); }
    }
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

equals は反射的でなければなりません

正しい equals() には4つの性質があります。1つ目は反射性で、x.equals(x) は true でなければなりません。

これは当然のように思えますが、同一参照の場合を処理し忘れると、独自のロジックによって破られることがあります。

public class Main {
    public static void main(String[] args) {
        String x = "java";
        System.out.println(x.equals(x));
    }
}

対称性と推移性

さらに2つのルールがあります:

  • 対称性:a.equals(b) ならば b.equals(a) となります。
  • 推移性:a.equals(b) かつ b.equals(c) ならば、a.equals(c) となります。

クラス階層をまたいで型を混在させると対称性が破られやすいため、クラスを慎重に比較してください。

public class Main {
    public static void main(String[] args) {
        String a = "x", b = "x", c = "x";
        boolean symmetric = a.equals(b) == b.equals(a);
        boolean transitive = a.equals(b) && b.equals(c) && a.equals(c);
        System.out.println("symmetric: " + symmetric);
        System.out.println("transitive: " + transitive);
    }
}

一貫性

4つ目のルールは一貫性です。オブジェクトが変化しない限り、繰り返し呼び出しても同じ結果を返さなければなりません。

そのため、equals と hashCode のもとにするフィールドには不変なものを使うべきです。挿入後に hashCode で使うフィールドを変更すると、マップが壊れてしまいます。

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        // Immutable record gives consistent equals/hashCode automatically
        record Id(int value) {}
        Id id = new Id(7);
        System.out.println(Objects.equals(id, new Id(7)));
        System.out.println(id.hashCode() == new Id(7).hashCode());
    }
}

レコードが自動で処理します

Java の record は、すべてのコンポーネントをもとに equals() と hashCode() を自動生成します。

値のように扱うデータには、レコードを優先してください。契約を自動的に満たせるうえ、常に整合性が保たれます。

import java.util.HashSet;
import java.util.Set;

public class Main {
    record Point(int x, int y) {}
    public static void main(String[] args) {
        Set<Point> set = new HashSet<>();
        set.add(new Point(1, 2));
        set.add(new Point(1, 2));
        System.out.println("size = " + set.size());
    }
}

可変キーの落とし穴

オブジェクトをマップに格納した後で、hashCode に使われるフィールドを変更すると、そのオブジェクトを見つけられなくなることがあります。

マップは古いバケットを検索しますが、オブジェクトは新しいバケットにハッシュされるようになっています。エントリは存在するのに見つからない、いわば失われた幽霊になります。

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class Main {
    static class Box {
        int id;
        Box(int id) { this.id = id; }
        @Override public boolean equals(Object o) {
            return o instanceof Box b && b.id == id;
        }
        @Override public int hashCode() { return Objects.hash(id); }
    }
    public static void main(String[] args) {
        Map<Box, String> m = new HashMap<>();
        Box key = new Box(1);
        m.put(key, "value");
        key.id = 99; // mutated after insertion
        System.out.println(m.get(key));
    }
}

理解度チェック

契約を理解できているか確認してください。

まとめ

equals/hashCode の契約について学びました:

  • 等しいオブジェクトは等しいハッシュコードを持たなければなりません。
  • equals は反射的、対称的、推移的、かつ一貫していなければなりません。
  • 両方のメソッドを同じ不変なフィールドにもとづいて実装してください。
  • レコードは正しい実装を自動的に生成します。

次は、正しい hashCode を手作業で実装します。

import java.util.Objects;

public class Main {
    public static void main(String[] args) {
        System.out.println("Contract recap: " + Objects.hash(1, 2, 3));
    }
}

よくある質問

「equals/hashCode の契約」レッスンは無料ですか?

はい。「equals/hashCode の契約」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Java Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Java Academyコースには全4レッスンが含まれています。

「equals/hashCode の契約」で何を学びますか?

両者が一致すべき理由を学びます。 ブラウザで直接実行するハンズオンコードでJava Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Java Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのJava Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン2/4です。

「equals/hashCode の契約」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このJava Academyレッスンでコードを書いて実行できますか?

はい。すべてのJava Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. HashMap の仕組み
  2. equals/hashCode の契約
  3. hashCode の実装
  4. ツリー化とパフォーマンス
← Java Academyに戻る