HashMap.getのnullがキー未登録か登録済みnullかをcontainsKeyで見分ける記事のアイキャッチ。

Java

JavaのHashMapでgetがnullになる原因|containsKeyとの使い分け

AIによる要約

HashMapのgetがnullを返すのは、キーが登録されていない場合と、キーに対してnull値が登録されている場合の2通りです。getだけでは区別できないため、区別が必要ならcontainsKeyを使います。getOrDefaultもnull値が登録されている場合はnullを返すため、常に既定値になるわけではありません。可能ならMapへnullを格納せず、未登録・取得失敗・値なしの意味を設計上分けます。

新人SE
新人SE
Map.getがnullでした。キーがなかったという意味で処理して大丈夫ですか?
ポンコツSE
ポンコツSE
HashMapでは、そのキーにnullが登録されている場合もgetはnullです。業務上区別が必要ならcontainsKeyも確認します。

Mapはマスタ値、表示名、集計結果、キャッシュなど幅広く使われます。`get(key) == null`を「キーなし」と決めつけると、登録済みだが値未設定という状態を見落とします。

逆に、毎回containsKeyしてからgetする必要もありません。null値を格納しない設計なら、getのnullを未登録として扱えます。重要なのはそのMapでnullが何を意味するかを決めることです。

この記事のポイント

  • HashMap.getのnullには2つの意味がある
  • containsKeyでキーの登録有無を区別できる
  • getOrDefaultは登録済みnullを既定値へ置き換えない
  • computeIfAbsentはnullを「値なし」として扱う
  • 可能ならMapへnullを入れない設計にする

XのJavaクイズで2種類のnullを確認する

HashMapへAliceのキーとnull値を登録し、AliceとBobをgetした結果を問うJavaクイズ画像。
Aliceはnull値を登録済み、Bobはキー自体がありません。
Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", null);

System.out.println(scores.get("Alice"));
System.out.println(scores.get("Bob"));

どちらもnullです。getの結果だけでは、AliceとBobの違いを判定できません。

HashMap.getはAliceもBobもnullだが、containsKeyではAliceがtrue、Bobがfalseになると説明する回答画像。
containsKeyなら、null値が登録済みかキー未登録かを区別できます。

Xの出題ポスト回答ポストも参照できます。

containsKeyでキーの有無を判定する

System.out.println(scores.containsKey("Alice")); // true
System.out.println(scores.containsKey("Bob"));   // false

HashMapはキーと値の組を保持します。Aliceはキーが存在し、対応する値がnullです。Bobはキーが存在しません。`containsKey`は値ではなくキーの登録有無を確認します。

Integer score = scores.get(name);

if (score != null) {
    return "点数: " + score;
}
if (scores.containsKey(name)) {
    return "採点前";
}
return "受験者なし";

この例では、数値あり、キーありnull、キーなしに別の意味を持たせています。業務仕様で3状態が本当に必要な場合だけ、この区別を使います。

getOrDefaultも登録済みnullには効かない

Map<String, String> labels = new HashMap<>();
labels.put("A", null);

System.out.println(
        labels.getOrDefault("A", "未設定")); // null
System.out.println(
        labels.getOrDefault("B", "未設定")); // 未設定

`getOrDefault`が既定値を返すのは、キーが存在しない場合です。キーAは存在するため、登録値nullがそのまま返ります。「nullなら常に既定値」というAPIではありません。

nullも既定値へ置き換えたいなら、要件を明示して処理します。

String label = labels.get(code);
if (label == null) {
    label = "未設定";
}

ただし、この書き方ではキーなしと登録済みnullを同じ扱いにしています。それで正しいか確認します。

computeIfAbsentでキャッシュする時の注意

User user = cache.computeIfAbsent(
        userId,
        id -> userRepository.findById(id)
                .orElse(null));

`computeIfAbsent`はキーに値がない、または現在の値がnullの場合に計算処理を呼びます。ただし計算結果がnullなら、そのnullを新しい値として登録しません。次回もRepository検索が走ります。

「見つからなかったこと」もキャッシュしたいなら、Optionalや専用の結果型を値にする方法があります。

Map<Long, Optional<User>> cache = new HashMap<>();

Optional<User> user = cache.computeIfAbsent(
        userId,
        userRepository::findById);

ただしキャッシュの有効期限、更新、メモリ上限、同時実行まで考える必要があります。本番用途ではCaffeineやSpring Cacheなど案件標準の仕組みを優先し、HashMapで独自キャッシュを作らない方が安全です。

Mapへnullを入れない設計を優先する

3状態が不要なら、nullを格納しない方が呼び出し側は単純になります。

Map<String, Integer> scores = new HashMap<>();

// 採点済みだけ登録する
if (score != null) {
    scores.put(studentName, score);
}

未採点を表したいなら、nullではなく状態を持つ型にできます。

record ScoreResult(
        ScoreStatus status,
        Integer score) {
}

enum ScoreStatus {
    NOT_GRADED,
    GRADED
}

少しコードは増えますが、nullの意味をコメントで補わず型で表せます。業務上重要な状態ほど、nullへ複数の意味を詰め込まないようにします。

HashMap以外ではnullの仕様が違う

Mapインターフェースを実装するすべてのクラスがnullを許すわけではありません。たとえば`ConcurrentHashMap`はnullキーとnull値を許可しません。`Map.of`もnullを許可しません。

Map<String, String> map = new ConcurrentHashMap<>();
map.put("A", null); // NullPointerException

Map<String, String> fixed = Map.of(
        "A", null); // NullPointerException

変数型がMapでも、具体的な生成方法で挙動が変わります。後からHashMapをConcurrentHashMapへ変更する予定があるなら、最初からnullを入れない契約にしておくと移行しやすくなります。

テストで3状態を固定する

@Test
void 登録済みnullとキー未登録を区別する() {
    Map<String, Integer> scores = new HashMap<>();
    scores.put("Alice", null);

    assertNull(scores.get("Alice"));
    assertTrue(scores.containsKey("Alice"));

    assertNull(scores.get("Bob"));
    assertFalse(scores.containsKey("Bob"));
}

@Test
void getOrDefaultは登録済みnullを返す() {
    Map<String, String> labels = new HashMap<>();
    labels.put("A", null);

    assertNull(labels.getOrDefault("A", "未設定"));
    assertEquals(
            "未設定",
            labels.getOrDefault("B", "未設定"));
}

キーありの値、キーありnull、キーなしを分けてテストします。nullを禁止する設計なら、putする境界で`Objects.requireNonNull`を使うテストも有効です。

現場レビューでよくある指摘

// レビューコメント例
getがnullでも、キー未登録と登録済みnullの両方が考えられます。
このMapで必要な状態を確認し、必要ならcontainsKeyで分けてください。

// レビューコメント例
getOrDefaultはキーAにnullが登録されている場合、既定値を返しません。
nullも置換する要件なら明示的に処理してください。

// レビューコメント例
nullへ「未検索」と「検索済み0件」の2つの意味を持たせています。
結果型かOptionalで状態を分けたいです。

提出前のセルフチェック

  • Mapのnullが何を意味するか決めたか
  • キーなしと登録済みnullを区別する必要があるか
  • 必要ならcontainsKeyを使ったか
  • getOrDefaultの登録済みnull挙動を理解したか
  • computeIfAbsentの計算結果がnullにならないか
  • Mapへnullを入れない設計にできないか
  • 具体実装がnullを許可するか確認したか
  • 3状態をテストしたか

Mapとnullを基礎から整理する参考書

Mapの主要APIと実装クラスの違いを体系的に押さえると、null判定やキャッシュ処理の判断が安定します。

書籍「スッキリわかるJava入門 第5版」の表紙
商品画像:Amazon.co.jp
PR Java初学者向け

スッキリわかるJava入門 第5版

中山 清喬 (著), 国本 大悟 (著), 株式会社フレアリンク (監修)

Java文法とオブジェクト指向を、最初から整理する一冊。

豊富な図とサンプルで、変数、条件分岐、クラス、例外処理まで順番に学べます。Javaの最初の教科書を探している人向けです。

  • Javaを基礎から体系的に学びたい
  • Java 25 LTS対応の入門書を選びたい

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

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

まとめ

HashMap.getがnullを返すのは、キーが存在しない場合と、キーにnull値が登録されている場合の2通りです。区別が必要ならcontainsKeyを使います。

getOrDefaultやcomputeIfAbsentにもnull固有の挙動があります。可能ならMapへnullを格納せず、未登録・検索済み0件・値未設定を型や状態で分け、テストで契約を固定しましょう。

-Java
-, , ,