Java 逆引き集 | Immutable key の要件(hashCode/equals 安定) — Map の鍵設計

Java Java
スポンサーリンク

Immutable key の要件(hashCode/equals 安定) — Map の鍵設計

Map のキーは「一度入れたら同じまま」であることが大前提です。途中で変化すると検索できなくなったり、誤った上書き・重複が起きます。特に HashMaphashCodeequals を使ってキーを管理するため、この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 が正しく機能する必要がある。
  • equalshashCode の関係: 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 必須)、並列なら ConcurrentHashMapHashMap のキー管理は equals/hashCode に依存することを理解して選ぶ。

まとめ

  • キーは不変で、equalshashCode が安定・整合していることが必須。
  • 可変要素や外部変更が入り込む構造は防御的コピーで遮断し、正規化で比較の揺れをなくす。
  • record・final フィールド・テンプレートを活用して、安全な「鍵設計」を当たり前にする。
  • HashMap のキー管理は equals/hashCode 前提で動くため、この関係性を理解しておくことが Map を使いこなす最短ルート。
タイトルとURLをコピーしました