Java 逆引き集 | Map の null キー/値対応(実装差) — 入出力検証

Java Java
スポンサーリンク

Map の null キー/値対応(実装差) — 入出力検証

Map は実装ごとに「null を許すか」が違います。キーに null を入れると検索や equals/hashCode まわりで混乱しやすいので、実装差を理解して「入出力を正しく検証」するのが安全策です。


実装ごとの null 対応差

実装null キーnull 値備考
HashMap可(最大1個のキーが null でも可)可(複数可)一般的な選択肢
LinkedHashMap順序保持あり
TreeMap不可(比較により例外)キー比較に Comparator/Comparable必須
Hashtable不可不可古い同期マップ。非推奨
ConcurrentHashMap不可不可並行用途の定番
EnumMap不可(キーは enum 必須)高速・省メモリ
WeakHashMapガベージ連動
IdentityHashMap参照同一性で比較
Map.of / Map.copyOf不可不可不変マップ生成系

直感の目安: 「並行系」「不変系」「ソート系」は null キーを嫌う。HashMap 系は基本ゆるい。


get が返す null の意味と安全な判定

  • get の戻り値が null の2通りの意味:
    • ラベル: キーが存在しない
    • ラベル: キーはあるが値が null
  • 安全判定:
    • containsKey を併用: キーの有無を確かめる
    • getOrDefault を使用: デフォルト値で曖昧さを回避
    • Optional を包む: APIで返すときは Optional<V> を検討
Map<String, Integer> m = new HashMap<>();
m.put("existsNull", null);

// 曖昧(null)
Integer v = m.get("existsNull");

// 安全な判定
boolean hasKey = m.containsKey("existsNull"); // true
int val = m.getOrDefault("missing", -1);      // -1(デフォルト)
Java

入出力検証の定石(API 設計)

  • 入力側(put する前)
    • ラベル: 実装が null キー/値を許すかを前提で決める
    • ラベル: 受け取り API では「null 禁止」を明確化(必要なら Objects.requireNonNull
    • ラベル: 不変マップを返す API は null を受け取らない(Map.of は NPE を投げる)
  • 出力側(get で返す)
    • ラベル: null 値があり得るなら Optional/デフォルト値を選択
    • ラベル: ドメインとして「null 値を許さない」設計にして曖昧さを無くす
// 入力バリデーション
void putSafe(Map<String, String> m, String k, String v) {
    Objects.requireNonNull(k, "key must not be null");
    Objects.requireNonNull(v, "value must not be null");
    m.put(k, v);
}

// 出力設計(Optional)
Optional<String> find(Map<String, String> m, String k) {
    return Optional.ofNullable(m.get(k));
}
Java

よくあるケース別の選び方

  • 並行処理(複数スレッド):
    • ラベル: ConcurrentHashMap(null キー/値不可)
    • 理由: null を禁止することで「存在判定の曖昧さ」や API 誤用を減らす
  • 順序保持(キャッシュ・LRUなど):
    • ラベル: LinkedHashMap(null 可)
    • 注意: null 値運用なら get 判定に containsKey を併用
  • キーの自然順/比較が必要:
    • ラベル: TreeMap(null キー不可・null 値可)
    • 理由: 比較器が null キーを扱えないのが一般的
  • 不変の設定表や定数表を返したい:
    • ラベル: Map.of / Map.copyOf(null 全禁止)
    • 理由: API の安全性を高め、変更不能にする

コード例で理解する

例1: HashMap(null キー/値の挙動)

Map<String, Integer> m = new HashMap<>();
m.put(null, 1);     // nullキー許可
m.put("a", null);   // null値許可

System.out.println(m.get(null)); // 1
System.out.println(m.get("a"));  // null(キーは存在)
System.out.println(m.containsKey("a")); // true
Java

例2: ConcurrentHashMap(null 禁止)

var cm = new java.util.concurrent.ConcurrentHashMap<String, String>();
// cm.put(null, "x");  // 実行時例外(null不可)
// cm.put("k", null);  // 実行時例外(null不可)
Java

例3: TreeMap(null キー不可・値は可)

var tm = new java.util.TreeMap<String, Integer>();
// tm.put(null, 1);   // 例外(キー比較できない)
tm.put("x", null);    // 値のnullは許可
Java

例4: 不変マップ(null 全禁止)

// Map.of は nullキー・値ともに不可(作成時に例外)
Map<String, Integer> constMap = Map.of("A", 1, "B", 2);
// Map.copyOf も同様に null 禁止(入力に null が含まれると例外)
Java

テンプレート集(そのまま使える形)

  • get の安全取得
V v = map.get(key);
if (v == null && !map.containsKey(key)) {
    // キーが無い
}
Java
  • デフォルト値で取得
V v = map.getOrDefault(key, defaultValue);
Java
  • null 禁止ポリシー(入力バリデーション)
map.put(Objects.requireNonNull(key), Objects.requireNonNull(value));
Java
  • 不変・非 null な設定を返す
return Map.of("host", "localhost", "port", "8080"); // すべて非null
Java

落とし穴と回避策

  • get の null で誤判定:
    • 回避: containsKey と併用、または「値に null を入れない」方針にする。
  • 不変/並行マップへ null を入れようとして例外:
    • 回避: 実装のポリシーを前提に設計(ConcurrentHashMap・Map.of は null 全禁止)。
  • TreeMap の null キー:
    • 回避: キー比較に null を使わない。どうしても扱うならラッパ型で正規化(例:空文字に置換)。
  • API の曖昧さ(null 値を返す):
    • 回避: Optional/明示的な結果型(Result)で表現し、null を返さない設計へ。

まとめ

  • 実装差を理解し、「null キー/値」を許すかを事前に決める。並行・不変・ソート系は null を嫌うのが原則。
  • get の null は「キー無し」と「値が null」を区別できないため、containsKey/getOrDefault/Optional で曖昧さを排除する。
  • API 設計では「入力は非 null、出力は非 null(または Optional)」を基本方針にし、例外や誤判定を未然に防ぐ。
タイトルとURLをコピーしました