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)」を基本方針にし、例外や誤判定を未然に防ぐ。
