AIによる要約
Map.putは追加した新しい値ではなく、そのキーに以前入っていた値を返します。初回登録なら通常null、同じキーを上書きした場合は上書き前の値です。ただし、null値を許すMapでは、未登録と以前の値がnullだった場合を戻り値だけで区別できません。上書きを禁止したいならcontainsKeyやputIfAbsent、既存時だけ更新するならreplaceなど、意図に合うAPIを選びます。
この記事はXへ投稿したJavaクイズの詳しい解説です。まず問題を解きたい方は、Xの元ポストを開くことができます。


Mapへ値を登録するputは基本APIですが、戻り値を新しい値だと思い込むと処理結果を逆に判断します。戻り値は上書き検知や以前の状態の復元に使えます。
一方、nullを値として許すHashMapでは、初回登録と以前の値nullが同じ戻り値になります。登録、更新、上書き禁止のどれをしたいかを明確にし、put以外のAPIも使い分けます。
この記事のポイント
- putは上書き前の値を返す
- 初回登録では通常nullを返す
- 戻り値nullだけではキー未登録と登録済みnullを区別できない
- 上書き禁止ならputIfAbsent等を検討する
- 更新結果を新しい値として使うなら別途getするか変数を使う
まずはXのJavaクイズを確認する

Map<String, Integer> map = new HashMap<>();
System.out.println(map.put("A", 1));
System.out.println(map.put("A", 2));答えはnull、1です。1回目は以前の値がなく、2回目は以前の1を2で上書きするためです。

putが返すのは以前の値
Map.put(K key, V value)は、登録後の値ではなく、そのキーへ以前関連付けられていた値を返します。メソッド終了後のMapには引数valueが入っています。
Integer previous = map.put("A", 2);
Integer current = map.get("A");
System.out.println(previous); // 1
System.out.println(current); // 2戻り値と現在値を同じ変数名で扱うと読み違えます。previous、replacedなど、上書き前だと分かる名前を使います。
初回登録と登録済みnullは区別できない
HashMapへnullを値として登録できるため、putの戻り値がnullでも2通りあります。キーがなかった場合と、キーはあったが以前の値がnullだった場合です。
Map<String, Integer> map = new HashMap<>();
map.put("A", null);
System.out.println(map.put("A", 10)); // null
System.out.println(map.put("B", 10)); // null登録有無を区別するなら、put前のcontainsKeyを使うか、そもそもMapにnullを入れない設計にします。戻り値だけで『新規登録だった』と断定しないでください。
上書きを禁止したいとき
マスタ登録や一意キーの取込で、既存値を黙って上書きしたくない場合があります。単純なputでは上書き後にしか以前の値を受け取れません。事前確認またはputIfAbsentを検討します。
User existing = users.putIfAbsent(user.id(), user);
if (existing != null) {
throw new DuplicateUserException(user.id());
}putIfAbsentはキーに値がない場合に登録します。ただしnull値を含むMapでは挙動の理解が必要です。また、DBの一意制約をMapだけで代替できません。アプリ内重複確認と永続化層の制約を両方用意します。
既存キーだけ更新するならreplace
キーが存在するときだけ更新したいならreplaceで意図を表せます。存在しないキーを新規追加しません。
Integer previous = scores.replace("U001", 90);
if (previous == null) {
throw new UserNotFoundException("U001");
}ただし以前の値がnullの場合との曖昧さがあります。Mapのnull方針を先に決め、必要ならcontainsKeyと組み合わせます。条件付きreplaceを使えば、現在値が期待値と一致した場合だけ更新する表現もできます。
集計ではmergeやcomputeを検討する
件数集計のため、getしてnull判定してputするコードは長くなりがちです。mergeを使うと初回値と更新方法を1つの式で表せます。
Map<String, Integer> counts = new HashMap<>();
for (Order order : orders) {
counts.merge(order.status(), 1, Integer::sum);
}APIを短く使うことが目的ではありません。初回登録、既存値更新、null時の扱いをチームが理解できるかを確認します。複雑な業務判断をcomputeへ押し込むより、通常のifの方が読みやすい場合もあります。
Map.ofの戻り値へputできない
Map.ofで作ったMapは変更できません。putの戻り値以前に、呼び出し自体がUnsupportedOperationExceptionになります。
Map<String, Integer> fixed = Map.of("A", 1);
fixed.put("A", 2); // UnsupportedOperationExceptionメソッド引数がMap型でも、具体的な生成方法によって変更可否が異なります。受け取ったMapを書き換えるAPIは、その契約を明示するか、自分の管理下で可変コピーを作ります。
JUnitで登録・更新・nullを分ける
putのテストでは初回登録、既存値上書き、以前の値null、キー未登録を分けます。戻り値と最終的なMapの両方を確認します。
@Test
void 既存値を返して新しい値へ更新する() {
Map<String, Integer> map = new HashMap<>();
map.put("A", 1);
assertEquals(1, map.put("A", 2));
assertEquals(2, map.get("A"));
}現場レビューでよくある指摘
Map更新のレビューでは、putの戻り値だけでなく、新規登録、上書き、既存時だけ更新のどれを意図しているかを見ます。
// レビューコメント例
putの戻り値は新しい値ではなく以前の値です。
変数名をpreviousValueへ変更し、現在値は別に扱ってください。
// レビューコメント例
既存データを上書きしてよい仕様か確認できません。
上書き禁止ならputIfAbsentと重複エラーを検討してください。
// レビューコメント例
戻り値nullだけでは未登録と以前の値nullを区別できません。
Mapへnullを許す契約とcontainsKeyの要否を整理してください。putを別APIへ機械的に置き換えるのではなく、登録と更新の業務ルールを明らかにします。DB更新と同じく、上書きしてよい条件をコードとテストへ残します。
提出前のセルフチェック
レビュー前に確認すること
- 戻り値が以前の値だと理解しているか
- 現在値と以前の値の変数名を分けたか
- 新規登録と更新を区別する必要があるか
- 上書きを許可する仕様か
- 登録済みnullと未登録を区別したか
- putIfAbsent・replace・mergeが意図に合うか
- 変更不可能なMapへputしていないか
- 戻り値と最終状態をテストしたか
Map APIを実務で使い分ける参考書
Mapにはput以外にもputIfAbsent、replace、merge、computeがあります。基本と実践を体系的に確認すると、更新意図に合うAPIを選びやすくなります。
スッキリわかるJava入門 実践編 第5版
基礎文法の次に必要な、現場寄りのJava知識を補う。
コレクション、ジェネリクス、ラムダ式、ストリームなど、業務コードで出会いやすい機能を入門編の次に学べます。
- Java基礎の次に何を学ぶか迷っている
- コレクションやStreamを整理したい
当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。
この記事とあわせて読みたい
まとめ
Map.putは新しい値ではなく、上書き前の値を返します。初回登録では通常null、同じキーを上書きすると以前の値が返り、処理後のMapには新しい値が入ります。
null値を許すMapでは戻り値nullだけで未登録を判断できません。上書きを禁止するならputIfAbsent、既存時だけならreplace、集計ならmergeなど、登録意図に合うAPIを選んでください。
