Java

HashMapで値が取れない原因はequalsかも

AIによる要約

HashMapに値を入れたのにgetで取れない場合、キーに使っているクラスのequalshashCodeが原因かもしれません。この記事では、初学者向けにMapの仕組みと現場で起きる失敗を整理します。

新人SE
新人SE
HashMapにputしたはずなのに、getするとnullになります。何から見ればいいですか?
ポンコツSE
ポンコツSE
まずキーに使っているクラスを見ます。equalsとhashCodeが揃っていない、またはキーの値を後から変えていると、Mapから見つけられなくなります。

業務システムでは、ユーザーID、商品コード、注文番号などをキーにしてHashMapへ値を入れることがあります。

ところが、自分で作ったクラスをキーにした瞬間に、putした値がgetできない問題が起きることがあります。

  • HashMapはキーをhashCodeequalsで探す
  • 同じ値に見えても、同一判定が壊れていると値を取得できない
  • Mapのキーにするクラスは、値が変わらない設計にする
  • レビューでは、キー用クラスの比較基準と変更可能性を見られる

まずHashMapはキーをどう探すのか

HashMapは、キーをそのまま順番に全部見て探しているわけではありません。まずhashCodeで大まかな場所を探し、その場所の中でequalsを使って同じキーかを確認します。

そのため、equalsでは同じに見えるのにhashCodeが違う、またはhashCodeが同じでもequalsが違う、という状態になると期待通りに値を取れません。

HashMapがhashCodeで置き場所を探しequalsで同じキーか確認する流れを示す図解。
HashMapはhashCodeで場所を探し、equalsで同じキーか確認します。
Map<UserId, String> names = new HashMap<>();

names.put(new UserId("U001"), "田中");

String name = names.get(new UserId("U001"));
System.out.println(name);

見た目には同じU001を指定しています。しかし、UserIdequalshashCodeを正しく実装していなければ、get結果はnullになることがあります。

よくある原因1:equalsだけ実装している

Java新人がやりがちなのは、equalsだけを実装してhashCodeを忘れるパターンです。Listcontainsでは動いたように見えるため、HashMapで初めて問題に気づくことがあります。

public class UserId {
    private final String value;

    public UserId(String value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof UserId other)) {
            return false;
        }
        return Objects.equals(this.value, other.value);
    }
}

このクラスはequalsだけを上書きしています。HashMapHashSetで使うなら、hashCodeも同じ基準で実装する必要があります。

public class UserId {
    private final String value;

    public UserId(String value) {
        this.value = Objects.requireNonNull(value);
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (!(obj instanceof UserId other)) {
            return false;
        }
        return Objects.equals(this.value, other.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

大事なのは、equalsで比較したvalueと、hashCodeに使うvalueが揃っていることです。

よくある原因2:Mapに入れたあとでキーを変更している

もう1つ現場で危ないのが、キーに使っているオブジェクトの値を後から変更するケースです。HashMapは登録時のハッシュ値を前提に管理しているため、キーの中身が変わると取り出せなくなることがあります。

UserKey key = new UserKey("U001");
Map<UserKey, String> names = new HashMap<>();

names.put(key, "田中");

key.setValue("U002");

System.out.println(names.get(key)); // 取れないことがある

これはかなり厄介です。コード上は同じkey変数を使っているので、初学者ほど原因に気づきにくくなります。

Mapのキーにするなら避けたい設計

  • setterでキー値を変更できる
  • equalshashCodeに使うフィールドが可変
  • 業務上の一時状態までキーの同一判定に含めている
  • DTOやEntityをそのままMapのキーにしている

現場ではどんな不具合になるか

たとえば、注文番号をキーにして明細を集約する処理を考えます。注文番号が同じなら同じ注文としてまとめたいのに、キーの同一判定が壊れていると、同じ注文が複数件として扱われます。

Map<OrderNo, List<OrderDetail>> detailMap = new HashMap<>();

for (OrderDetail detail : details) {
    OrderNo orderNo = new OrderNo(detail.getOrderNo());
    detailMap.computeIfAbsent(orderNo, key -> new ArrayList<>()).add(detail);
}

このような処理でOrderNoequals/hashCodeが壊れていると、集約結果がズレます。画面表示だけならまだ気づけるかもしれませんが、請求、在庫、帳票に流れると影響が大きくなります。

レビューでは何を見られるのか

レビューでは、HashMapを使っていること自体よりも、キーにしているクラスが安全かを見られます。特に、新人のコードでは「そのクラスをMapのキーにして大丈夫か」という観点でコメントが付きます。

// レビューコメント例
// UserSearchConditionをHashMapのキーにしていますが、setterで値が変わります。
// Map登録後に値が変わると取得できなくなる可能性があるため、
// キー専用の不変クラスを作るか、StringのuserIdをキーにしてください。

このコメントは、文法の指摘ではありません。業務データの集約や検索結果が壊れないようにするための設計指摘です。

迷ったときの判断基準

HashMapのキーにしてよいか確認する

  • キーに使う値が業務上一意か
  • キー値が生成後に変わらないか
  • equalshashCodeが同じフィールドを見ているか
  • DTOやEntityを流用せず、キー専用クラスにできないか
  • 単純にStringLongをキーにした方が読みやすくないか

新人のうちは、無理に自作クラスをMapのキーにしない判断も大事です。注文番号やユーザーIDがStringで十分なら、まずは単純な型をキーにする方が読みやすいこともあります。

この記事とあわせて読みたい

まとめ

HashMapで値が取れないときは、putgetのコードだけでなく、キーに使っているクラスのequalshashCodeを確認します。特に、Mapのキーは後から値が変わらない設計にすることが重要です。

-Java
-, , , ,