hashCode の実装
正しいハッシュ関数を記述します。
「hashCode の実装」はCoddyKit上の無料Java Academyレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはJava Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Java Academyコースには全4レッスンが含まれています。
優れた hashCode の目標
優れた hashCode() には、次の性質があります:
- 等しいオブジェクトに対して同じ値を返す(契約)。
- 等しくないオブジェクトを多くの異なる値に分散させる。
- 計算コストが低い。
常に同じ値を返す粗末な hashCode でも契約は満たしますが、マップが遅い連結リストになってしまいます。
public class Main {
public static void main(String[] args) {
// Legal but terrible: every object collides
System.out.println("constant hashCode is legal but kills performance");
}
}一般的なケースでは Objects.hash
最も簡単で正しい方法は Objects.hash(field1, field2, ...) です。
null も処理でき、標準的なアルゴリズムでフィールドを組み合わせます。equals で比較する同じフィールドを使ってください。
import java.util.Objects;
public class Main {
static class User {
final String name; final int age;
User(String name, int age) { this.name = name; this.age = age; }
@Override public int hashCode() { return Objects.hash(name, age); }
}
public static void main(String[] args) {
User a = new User("Ada", 36);
User b = new User("Ada", 36);
System.out.println(a.hashCode() == b.hashCode());
}
}定番の31倍乗算
ハッシュを手作業で実装する場合、累積結果に31を掛けて、各フィールドのハッシュを加えるのが標準的なパターンです。
31は奇数の素数です。また、31 * x は (x << 5) - x と同じなので、JVM はこれを最適化できます。
public class Main {
static class User {
final String name; final int age;
User(String name, int age) { this.name = name; this.age = age; }
@Override public int hashCode() {
int result = 17;
result = 31 * result + (name == null ? 0 : name.hashCode());
result = 31 * result + age;
return result;
}
}
public static void main(String[] args) {
System.out.println(new User("Ada", 36).hashCode());
}
}プリミティブ型のハッシュ化
各プリミティブ型には、推奨されるハッシュ化方法があります:
int:値そのものを使います。long:(int)(value ^ (value >>> 32))を使います。boolean:1または0を使います。double:Double.hashCode(value)を使います。
public class Main {
public static void main(String[] args) {
long id = 4_000_000_000L;
int longHash = (int) (id ^ (id >>> 32));
System.out.println("long hash: " + longHash);
System.out.println("double hash: " + Double.hashCode(3.14));
System.out.println("bool hash: " + Boolean.hashCode(true));
}
}配列のハッシュ化
配列に対して直接 hashCode() を呼び出してはいけません。内容ではなく、同一性にもとづいてハッシュ化されるためです。
1次元配列には Arrays.hashCode(arr) を、ネストした配列には Arrays.deepHashCode(arr) を使ってください。
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] a = {1, 2, 3};
int[] b = {1, 2, 3};
System.out.println("identity equal: " + (a.hashCode() == b.hashCode()));
System.out.println("content equal: " + (Arrays.hashCode(a) == Arrays.hashCode(b)));
}
}equals と hashCode の整合性を保つ
hashCode() で使うフィールドは、equals() で使うフィールドの部分集合でなければなりません(理想的にはまったく同じフィールドにします)。
equals が hashCode より多くのフィールドを比較する場合、等しいオブジェクトは同じハッシュを持つので問題ありません。ただし、hashCode が equals で無視するフィールドを使うと、契約に違反します。
import java.util.Objects;
public class Main {
static class Coord {
final int x, y;
Coord(int x, int y) { this.x = x; this.y = y; }
@Override public boolean equals(Object o) {
return o instanceof Coord c && c.x == x && c.y == y;
}
@Override public int hashCode() { return Objects.hash(x, y); }
}
public static void main(String[] args) {
Coord a = new Coord(3, 4), b = new Coord(3, 4);
System.out.println(a.equals(b) && a.hashCode() == b.hashCode());
}
}ハッシュのキャッシュ
ハッシュ化にコストがかかる不変オブジェクトでは、結果をフィールドにキャッシュできます。
String は内部でまさにこの処理を行っています。キャッシュされた値が古くならないよう、オブジェクトが本当に不変である場合にだけ使ってください。
import java.util.Objects;
public class Main {
static final class Key {
final String a, b;
private int hash; // 0 until computed
Key(String a, String b) { this.a = a; this.b = b; }
@Override public int hashCode() {
int h = hash;
if (h == 0) { h = Objects.hash(a, b); hash = h; }
return h;
}
}
public static void main(String[] args) {
Key k = new Key("x", "y");
System.out.println(k.hashCode());
System.out.println(k.hashCode());
}
}分布が重要です
適切に分散された hashCode は、キーをバケット全体に均等に広げます。ここでは、複数のオブジェクトについて異なるハッシュコードの数を数えます。
異なる値が多いほど衝突が減り、マップは高速になります。
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class Main {
record Pair(int a, int b) {}
public static void main(String[] args) {
Set<Integer> hashes = new HashSet<>();
for (int i = 0; i < 100; i++) {
hashes.add(Objects.hash(i, i * 7));
}
System.out.println("distinct hashes: " + hashes.size());
}
}分布が悪い例
乗算せずにフィールドを合計すると衝突が発生します。(1,2) と (2,1) はどちらも3にハッシュされます。
31倍の乗算を使うと順序が結果に影響するため、この対称性が崩れます。
public class Main {
static int badHash(int a, int b) { return a + b; }
static int goodHash(int a, int b) { return 31 * a + b; }
public static void main(String[] args) {
System.out.println("bad (1,2): " + badHash(1, 2) + ", (2,1): " + badHash(2, 1));
System.out.println("good (1,2): " + goodHash(1, 2) + ", (2,1): " + goodHash(2, 1));
}
}値型にはレコードを優先する
純粋なデータ保持用クラスには、record が正しく、分布のよい hashCode を自動生成します。
独自の意味付けが必要な場合や、レコードを使えない場合にだけ、hashCode を手作業で実装してください。
public class Main {
record Money(long cents, String currency) {}
public static void main(String[] args) {
Money a = new Money(1099, "USD");
Money b = new Money(1099, "USD");
System.out.println(a.equals(b));
System.out.println(a.hashCode() == b.hashCode());
}
}すべてを組み合わせる
完全な値クラスとは、不変なフィールド、同じフィールドにもとづく equals と hashCode、そして適切な toString を備えたクラスです。
import java.util.Objects;
public class Main {
static final class Version {
final int major, minor, patch;
Version(int major, int minor, int patch) {
this.major = major; this.minor = minor; this.patch = patch;
}
@Override public boolean equals(Object o) {
return o instanceof Version v && v.major == major && v.minor == minor && v.patch == patch;
}
@Override public int hashCode() { return Objects.hash(major, minor, patch); }
@Override public String toString() { return major + "." + minor + "." + patch; }
}
public static void main(String[] args) {
Version v = new Version(2, 1, 0);
System.out.println(v + " hash=" + v.hashCode());
}
}理解度チェック
hashCode のスキルを確認してください。
まとめ
hashCode を正しく実装する方法を学びました:
- 一般的なケースでは
Objects.hash(...)を使います。 - 手作業でハッシュ化する場合は31倍乗算のパターンを使います。
- 配列にはデフォルトの方法ではなく、
Arrays.hashCodeを使います。 - hashCode で使うフィールドを equals と一致させ、レコードを優先してください。
次は、Java 8以降で衝突の多いバケットがどのようにツリー化されるかを見ていきます。
import java.util.Objects;
public class Main {
public static void main(String[] args) {
System.out.println("hashCode recap done: " + Objects.hash("done"));
}
}よくある質問
「hashCode の実装」レッスンは無料ですか?
はい。「hashCode の実装」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Java Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Java Academyコースには全4レッスンが含まれています。
「hashCode の実装」で何を学びますか?
正しいハッシュ関数を記述します。 ブラウザで直接実行するハンズオンコードでJava Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Java Academyを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのJava Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。
「hashCode の実装」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このJava Academyレッスンでコードを書いて実行できますか?
はい。すべてのJava Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- HashMap の仕組み
- equals/hashCode の契約
- hashCode の実装
- ツリー化とパフォーマンス