Immutable key の要件(hashCode/equals 安定) — Map の鍵設計
Map のキーは「一度入れたら同じまま」であることが大前提です。途中で変化すると検索できなくなったり、誤った上書き・重複が起きます。特に HashMap は hashCode と equals を使ってキーを管理するため、この2つの「安定性」と「整合性」が鍵設計の要です。また、HashMap はキー一意・値重複可・順序保証なしなどの性質を持ち、equals/hashCode の理解と併せて扱うのが基本です。
キーの必須要件
- 不変性(immutable): キーに使うフィールドは Map に入れた後に変更しない。変更されると
hashCodeのバケットやequals判定が変わり、取得・削除ができなくなる。 - equals と hashCode の整合性:
- ラベル: 「equals が true なら hashCode も同じ」
- 説明:
HashMapは等価判定にequals、バケット選択にhashCodeを用いる。整合しないと衝突や探索失敗を招く。
- 同値性の規約遵守:
- ラベル: 反射性・対称性・推移性・一貫性
- 説明:
equalsの基本契約に従い、比較が状況で変化しないようにする。
- 比較に使う全フィールドを反映:
- ラベル: 「キーの同値性に関わる全ての不変フィールドを
equals/hashCodeに含める」 - 説明: 一部だけ含めると衝突や誤同値判定になる。
- ラベル: 「キーの同値性に関わる全ての不変フィールドを
- null/NaN/大小文字の扱いを明示:
- ラベル: 事前正規化(例:小文字化・トリム・null 禁止)
- 説明: 比較が環境や入力で揺れないようにする。
失敗例と修正
失敗例: 可変フィールドをキーに使ってしまう
class MutableKey {
String code; // 後で変わる可能性がある
// equals/hashCode は code に依存
@Override public boolean equals(Object o) { return o instanceof MutableKey mk && Objects.equals(code, mk.code); }
@Override public int hashCode() { return Objects.hash(code); }
}
// Map に入れた後に code を変更すると、get/remove に失敗する
Java修正: 不変化+防御的コピー
final class Key {
private final String code; // 不変
private final List<String> tags; // 外部から防御的コピー
Key(String code, List<String> tags) {
this.code = Objects.requireNonNull(code);
this.tags = List.copyOf(tags); // 外部変更を遮断
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Key k)) return false;
return code.equals(k.code) && tags.equals(k.tags);
}
@Override public int hashCode() { return Objects.hash(code, tags); }
}
Javaよく使う安全なキーの作り方
- レコード(Java 16+):
- ラベル:
recordは自動で不変・equals/hashCode生成 - 例:
- ラベル:
public record UserKey(String id, int branch) {}
// これだけで Map のキーとして安全(フィールドが不変かつ equals/hashCode 一貫)
Java- 不変クラス+final フィールド:
- ラベル: すべて
private final、セッターなし - 効果: 生成後に値が変わらず、比較の安定性を担保。
- ラベル: すべて
- 防御的コピー:
- ラベル: コレクションや配列は
List.copyOf,Set.copyOf,Arrays.copyOf - 効果: 外部からの変更がキーへ伝播しない。
- ラベル: コレクションや配列は
- 正規化の一貫性:
- ラベル: 例:メールアドレスキーは小文字化・トリムして格納
- 効果: equals の揺れを防ぐ。
HashMap の理解を要件に結びつける
- キー一意・値重複可: 同じキーは上書き、値の重複は許可。順序は保持されない(順序必要なら
LinkedHashMap)。この仕様の下でequals/hashCodeが正しく機能する必要がある。 equalsとhashCodeの関係: Map の検索・格納はhashCodeでバケットを選び、その中でequalsで同値を確認する。両者の整合性が崩れると探索や衝突解決が破綻する。
例題で身につける
例題1: 複合キー(id+版数)で辞書化
public record DocKey(String id, int version) {}
Map<DocKey, String> store = new HashMap<>();
store.put(new DocKey("A001", 1), "draft");
store.put(new DocKey("A001", 2), "final");
System.out.println(store.get(new DocKey("A001", 2))); // final
Java- ポイント: record による不変+自動
equals/hashCode。
例題2: 入力正規化をキーに組み込む
final class EmailKey {
private final String normalized;
EmailKey(String raw) {
this.normalized = Objects.requireNonNull(raw).trim().toLowerCase(Locale.ROOT);
}
@Override public boolean equals(Object o) { return o instanceof EmailKey k && normalized.equals(k.normalized); }
@Override public int hashCode() { return normalized.hashCode(); }
}
Java- ポイント: 表記ゆれを消して、一貫したキーに。
例題3: コレクションを含むキーは防御的コピー
final class RouteKey {
private final List<String> hops;
RouteKey(List<String> hops) { this.hops = List.copyOf(hops); } // 不変リストに固定
@Override public boolean equals(Object o) { return o instanceof RouteKey k && hops.equals(k.hops); }
@Override public int hashCode() { return hops.hashCode(); }
}
Java- ポイント: 外部が持つリストを後から変更しても、キーは影響を受けない。
テンプレート集
- 不変キー(final フィールド+防御的コピー)
final class Key {
private final String a;
private final int b;
private final List<String> c;
Key(String a, int b, List<String> c) {
this.a = Objects.requireNonNull(a);
this.b = b;
this.c = List.copyOf(c);
}
@Override public boolean equals(Object o) { /* a,b,c をすべて比較 */ }
@Override public int hashCode() { return Objects.hash(a, b, c); }
}
Java- record で簡潔な複合キー
public record Key(String a, int b) {}
Java- 正規化をコンストラクタに集約
final class Key {
private final String k;
Key(String raw) { this.k = raw.trim().toLowerCase(Locale.ROOT); }
@Override public boolean equals(Object o) { /* k 比較 */ }
@Override public int hashCode() { return k.hashCode(); }
}
Java- equals/hashCode の安全比較(数値)
@Override public int compareTo(NumKey o) { return Integer.compare(this.code, o.code); }
@Override public int hashCode() { return Integer.hashCode(code); }
Java実務でのコツ
- 「変更されないものだけ」をキーにする: ID、版数、日時(固定)、正規化済み文字列など。
- 可変情報は除外するか別構造へ: 名前・説明・一時フラグなどはキーに入れない。
- 巨大オブジェクトは避ける: キーは軽量・比較が速いものに。必要ならキーを縮約(ハッシュだけにするのは非推奨、衝突時に比較できないため)。
- テストで検証: put→フィールド変更(シミュレーション)→get で失敗することを確認し、設計を不変化へ。
- Map 実装の選定: 順序が必要なら
LinkedHashMap、ソートが必要ならTreeMap(キーがComparableまたはComparator必須)、並列ならConcurrentHashMap。HashMapのキー管理はequals/hashCodeに依存することを理解して選ぶ。
まとめ
- キーは不変で、
equalsとhashCodeが安定・整合していることが必須。 - 可変要素や外部変更が入り込む構造は防御的コピーで遮断し、正規化で比較の揺れをなくす。
- record・final フィールド・テンプレートを活用して、安全な「鍵設計」を当たり前にする。
HashMapのキー管理はequals/hashCode前提で動くため、この関係性を理解しておくことが Map を使いこなす最短ルート。
