- JAVA
Java HashMapの使い方|ループ・getOrDefault・merge早見表
JavaのHashMapとは、「キー」と「値」をペアで保存し、キーを指定して値をすばやく取り出せるクラスです。 putで入れてgetで取り出すのが基本で、値が無いときの既定値はgetOrDefault、件数の集計はmerge、グループ分けはcomputeIfAbsentで短く書けます。
この記事では、主要メソッド20種の早見表と実行結果、ループ4種(entrySet・keySet・forEach・Stream)の比較、単語カウントとグループ分けのコピペ例、ConcurrentModificationExceptionの再現と直し方、LinkedHashMap・TreeMap・ConcurrentHashMap・Map.ofとの違いを解説します。コードはすべてOpenJDK 21.0.10で実行し、出力をそのまま貼り付けています。
Java HashMapの早見表(主要メソッド一覧)
まずはこの表で「やりたいこと」からメソッドを探してください。 「実行結果」列は、後述のMapMethods.javaをJava 21.0.10で動かしたときの戻り値です(在庫数を表すMap<String, Integer>で確認)。
| やりたいこと | メソッド | 戻り値 | 実行結果の例 |
|---|---|---|---|
| 追加・上書き | put(k, v) |
前の値(無ければnull) |
新規null/上書き10 |
| 値を取り出す | get(k) |
値(無ければnull) |
あり12/なしnull |
| 無いときの既定値つきで取り出す | getOrDefault(k, 既定値) |
値か既定値 | 0 |
| キーがあるか調べる | containsKey(k) |
boolean |
true |
| 値があるか調べる | containsValue(v) |
boolean(全件を走査) |
true |
| 無いときだけ追加 | putIfAbsent(k, v) |
前の値(新規ならnull) |
既存12/新規null |
| 加算・連結して更新(集計) | merge(k, v, 関数) |
更新後の値 | 既存13/新規1 |
| 計算して更新 | compute(k, (k, v) -> …) |
更新後の値 | 10 |
| 無いときだけ計算して追加 | computeIfAbsent(k, k -> …) |
現在の値 | 6 |
| あるときだけ計算して更新 | computeIfPresent(k, (k, v) -> …) |
更新後の値 | 20 |
| あるときだけ置き換え | replace(k, v) |
前の値(無ければnull) |
3/なしnull |
| 旧値が一致したら置き換え | replace(k, 旧, 新) |
boolean |
true |
| 削除 | remove(k) |
削除した値 | 1 |
| 値も一致したら削除 | remove(k, v) |
boolean |
false |
| 件数・空か | size()/isEmpty() |
int/boolean |
5/false |
| キー・値・ペアの一覧 | keySet()/values()/entrySet() |
ビュー(元のMapとつながる) | [eraser, note, …] |
| 全部の値を変換 | replaceAll((k, v) -> …) |
なし | {eraser=60, …} |
| ループ | forEach((k, v) -> …) |
なし | 後述 |
| 条件で削除 | entrySet().removeIf(…) |
boolean |
後述 |
| 全部消す | clear() |
なし | {} |
迷ったときの選び方は次のとおりです。
| 場面 | おすすめ | ひとこと |
|---|---|---|
無いキーでnullを避けたい |
getOrDefault |
数値の初期値0、文字列の初期値""に |
| 出現回数・合計を数える | merge(k, 1, Integer::sum) |
1行で「無ければ1、あれば+1」 |
| キーごとにリストへ追加 | computeIfAbsent(k, x -> new ArrayList<>()).add(v) |
グループ分けの定番 |
| 初回だけ登録したい | putIfAbsent |
戻り値は「前の値」なので初回はnull |
| ループ中に削除 | entrySet().removeIf |
拡張forの中でremoveしない |
| 複数スレッドで更新 | ConcurrentHashMap |
mergeが原子的に動く |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java HashMapの使い方|ループ・getOrDefault・merge早見表」
HashMapの基本(宣言・import・実行方法)
HashMapはjava.utilパッケージにあるクラスで、変数の型はMapインターフェースで受けるのが一般的です。 型をMapにしておくと、あとでLinkedHashMapやTreeMapに差し替えるときに1か所の変更で済みます。
import java.util.HashMap;
import java.util.Map;
Map<String, Integer> stock = new HashMap<>(); // キーがString、値がInteger
stock.put("pen", 10);
int n = stock.get("pen"); // 10
| 項目 | 内容 |
|---|---|
| 型の書き方 | Map<キーの型, 値の型>。intではなくIntegerなどを使う |
| キー | 重複できない。同じキーでputすると上書き |
| 値 | 重複できる。nullも入れられる |
| 順序 | 保証されない(追加順にもキー順にもならない) |
| 速さ | get・putは件数が増えてもほぼ一定時間(平均O(1)) |
この記事のコードは、クラス名と同じファイル名で保存し、java ファイル名.javaでそのまま実行できます(Java 11以降。端末はUTF-8)。
$ java -version
openjdk version "21.0.10" 2026-01-20
$ java MapMethods.java
※java -versionの1行目は手元の環境の表示です。2行目以降(ビルド番号など)は省略しています。
主要メソッドを全部動かして戻り値を確かめる
HashMapのメソッドは、戻り値を知っておくと1行で書ける処理が増えます。 特にput・putIfAbsent・replaceは「前の値」を返し、merge・computeは「更新後の値」を返す点がポイントです。
import java.util.HashMap;
import java.util.Map;
public class MapMethods {
public static void main(String[] args) {
Map<String, Integer> stock = new HashMap<>();
System.out.println("put(新規) : " + stock.put("pen", 10));
System.out.println("put(上書き) : " + stock.put("pen", 12));
stock.put("note", 5);
stock.put("clip", 30);
System.out.println("get(あり) : " + stock.get("pen"));
System.out.println("get(なし) : " + stock.get("tape"));
System.out.println("getOrDefault : " + stock.getOrDefault("tape", 0));
System.out.println("containsKey : " + stock.containsKey("note"));
System.out.println("containsValue : " + stock.containsValue(30));
System.out.println("putIfAbsent(既存): " + stock.putIfAbsent("pen", 99) + " -> " + stock.get("pen"));
System.out.println("putIfAbsent(新規): " + stock.putIfAbsent("tape", 3) + " -> " + stock.get("tape"));
System.out.println("merge(既存) : " + stock.merge("pen", 1, Integer::sum));
System.out.println("merge(新規) : " + stock.merge("glue", 1, Integer::sum));
System.out.println("compute : " + stock.compute("note", (k, v) -> v == null ? 1 : v * 2));
System.out.println("computeIfAbsent : " + stock.computeIfAbsent("eraser", k -> k.length()));
System.out.println("computeIfPresent : " + stock.computeIfPresent("clip", (k, v) -> v - 10));
System.out.println("replace : " + stock.replace("tape", 4));
System.out.println("replace(なし) : " + stock.replace("stapler", 1));
System.out.println("replace(旧値一致): " + stock.replace("tape", 4, 6));
System.out.println("remove : " + stock.remove("glue"));
System.out.println("remove(値一致) : " + stock.remove("tape", 999));
System.out.println("size / isEmpty : " + stock.size() + " / " + stock.isEmpty());
System.out.println("keySet : " + stock.keySet());
System.out.println("values : " + stock.values());
System.out.println("entrySet : " + stock.entrySet());
stock.replaceAll((k, v) -> v * 10);
System.out.println("replaceAll : " + stock);
stock.clear();
System.out.println("clear : " + stock + " isEmpty=" + stock.isEmpty());
}
}
実行結果(Java 21.0.10):
put(新規) : null
put(上書き) : 10
get(あり) : 12
get(なし) : null
getOrDefault : 0
containsKey : true
containsValue : true
putIfAbsent(既存): 12 -> 12
putIfAbsent(新規): null -> 3
merge(既存) : 13
merge(新規) : 1
compute : 10
computeIfAbsent : 6
computeIfPresent : 20
replace : 3
replace(なし) : null
replace(旧値一致): true
remove : 1
remove(値一致) : false
size / isEmpty : 5 / false
keySet : [eraser, note, tape, pen, clip]
values : [6, 10, 6, 13, 20]
entrySet : [eraser=6, note=10, tape=6, pen=13, clip=20]
replaceAll : {eraser=60, note=100, tape=60, pen=130, clip=200}
clear : {} isEmpty=true
putは上書き前の値10を返し、putIfAbsentは既存キーには何もせず、replaceは無いキーを追加しません。 keySetが追加順になっていない点も確認できます(HashMapは順序を保証しない)。
keySet()・values()・entrySet()はコピーではなく「ビュー」です。 ビューから要素を削除すると元のMapからも消えます。この性質は、後半のremoveIfで活用します。
getとgetOrDefaultの違い(キーが無いときとnullの扱い)
getはキーが無いとnullを返し、getOrDefaultは指定した既定値を返します。 ただし、キーがあって値がnullのときは、getOrDefaultもnullを返す点に注意が必要です。
import java.util.HashMap;
import java.util.Map;
public class NullPitfall {
public static void main(String[] args) {
Map<String, Integer> m = new HashMap<>();
m.put("a", null);
System.out.println("get(\"a\") = " + m.get("a"));
System.out.println("get(\"zzz\") = " + m.get("zzz"));
System.out.println("containsKey(\"a\") = " + m.containsKey("a"));
System.out.println("getOrDefault(\"a\", 0) = " + m.getOrDefault("a", 0));
System.out.println("putIfAbsent(\"a\", 7) = " + m.putIfAbsent("a", 7) + " -> " + m.get("a"));
m.put("b", 5);
m.merge("b", 0, (oldV, newV) -> null);
System.out.println("merge で null を返す = " + m + " (b が消える)");
m.put("c", 1);
m.compute("c", (k, v) -> null);
System.out.println("compute で null を返す = " + m + " (c が消える)");
m.put(null, 100);
System.out.println("null キー = " + m.get(null));
try {
int n = m.get("zzz");
System.out.println(n);
} catch (NullPointerException e) {
System.out.println("int に代入: " + e);
}
}
}
実行結果(Java 21.0.10):
get("a") = null
get("zzz") = null
containsKey("a") = true
getOrDefault("a", 0) = null
putIfAbsent("a", 7) = null -> 7
merge で null を返す = {a=7} (b が消える)
compute で null を返す = {a=7} (c が消える)
null キー = 100
int に代入: java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "java.util.Map.get(Object)" is null
この結果からわかる4つのルールです。
| ルール | 内容 | 対処 |
|---|---|---|
getのnullは2通り |
「キーが無い」と「値がnull」を区別できない |
区別したいときはcontainsKeyを使う |
getOrDefaultは「キーが無いとき」だけ既定値 |
値がnullで登録済みならnullが返る |
そもそもnullを値に入れない設計にする |
putIfAbsentは値がnullなら上書きする |
「無い」とみなされて7が入った |
初期化用途ならむしろ便利 |
merge・computeでnullを返すとキーが消える |
関数の戻り値nullは「削除」の合図 |
「0になったら消す」処理に使える |
int n = m.get(...)とintへ直接代入すると、キーが無いときNullPointerExceptionになります(Integer→intの自動変換のため)。数値のMapではgetOrDefault(key, 0)が安全です。
HashMapをループする4つの方法(同じデータで比較)
Mapのループは「entrySet」「keySet」「forEach」「Stream」の4通りです。迷ったらentrySetの拡張for、短く書くならforEachを選びます。 同じデータで4つを並べて比べます。表示順を固定するため、ここでは追加順を保つLinkedHashMapを使っています(メソッドはHashMapと同じです)。
import java.util.LinkedHashMap;
import java.util.Map;
public class MapLoops {
public static void main(String[] args) {
Map<String, Integer> score = new LinkedHashMap<>();
score.put("佐藤", 82);
score.put("鈴木", 95);
score.put("高橋", 67);
System.out.println("① entrySet + 拡張for");
for (Map.Entry<String, Integer> e : score.entrySet()) {
System.out.println(" " + e.getKey() + "=" + e.getValue());
}
System.out.println("② keySet + get");
for (String name : score.keySet()) {
System.out.println(" " + name + "=" + score.get(name));
}
System.out.println("③ forEach(BiConsumer)");
score.forEach((name, point) -> System.out.println(" " + name + "=" + point));
System.out.println("④ Stream(80点以上を点数の高い順)");
score.entrySet().stream()
.filter(e -> e.getValue() >= 80)
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(e -> System.out.println(" " + e.getKey() + "=" + e.getValue()));
}
}
実行結果(Java 21.0.10):
① entrySet + 拡張for
佐藤=82
鈴木=95
高橋=67
② keySet + get
佐藤=82
鈴木=95
高橋=67
③ forEach(BiConsumer)
佐藤=82
鈴木=95
高橋=67
④ Stream(80点以上を点数の高い順)
鈴木=95
佐藤=82
4つのループの比較表です。
| 方法 | 短さ | キーと値を同時に | break・returnで抜ける |
外側のローカル変数の更新 | 向いている場面 |
|---|---|---|---|---|---|
① entrySet+拡張for |
普通 | できる | できる | できる | 迷ったらこれ。途中で抜ける処理 |
② keySet+get |
普通 | getで再検索 |
できる | できる | キーだけ使うとき |
③ forEach |
短い | できる | できない | できない | 全件を表示・転記するだけ |
| ④ Stream | 処理による | できる | できない(filter等で表現) |
できない | 絞り込み・並べ替え・集計 |
keySet+getはループのたびにキーで再検索するので、両方使うならentrySetが素直です。 なお、100万件のHashMap<Integer, Integer>を4つの方法で合計する簡易計測(このマシン、2コア、6回まわした後半3回)では、どれも15〜90ミリ秒で回ごとに順位が入れ替わる程度の差でした。速さより「途中で抜けたいか」「並べ替えたいか」で選ぶのがおすすめです。
単語の出現回数を数える(mergeのコピペ例)
出現回数のカウントはmap.merge(キー, 1, Integer::sum)の1行が定番です。 「キーが無ければ1を入れる、あれば今の値に1を足す」を1回の呼び出しで行います。同じ処理をgetOrDefault版とStream版でも書いて比べます。
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
public class WordCount {
public static void main(String[] args) {
String text = "apple banana apple cherry banana apple";
String[] words = text.split(" ");
// 1. merge(おすすめ)
Map<String, Integer> count = new HashMap<>();
for (String w : words) {
count.merge(w, 1, Integer::sum);
}
System.out.println("merge : " + count);
// 2. getOrDefault
Map<String, Integer> count2 = new HashMap<>();
for (String w : words) {
count2.put(w, count2.getOrDefault(w, 0) + 1);
}
System.out.println("getOrDefault : " + count2);
// 3. Stream(Collectors.groupingBy + counting)。値は Long
Map<String, Long> count3 = Arrays.stream(words)
.collect(Collectors.groupingBy(Function.identity(), TreeMap::new, Collectors.counting()));
System.out.println("Stream : " + count3);
// 多い順に並べる(同数ならキーの順)
count.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed()
.thenComparing(Map.Entry.comparingByKey()))
.forEach(e -> System.out.println(" " + e.getKey() + " : " + e.getValue()));
}
}
実行結果(Java 21.0.10):
merge : {banana=2, cherry=1, apple=3}
getOrDefault : {banana=2, apple=3, cherry=1}
Stream : {apple=3, banana=2, cherry=1}
apple : 3
banana : 2
cherry : 1
数は同じですが、merge版とgetOrDefault版で表示順が違います。 appleとcherryは容量16のHashMapで同じ格納位置(バケット)に入るキーで(hashCodeから計算して確認)、その中の並びが追加の書き方で変わったためです。順序に意味を持たせたいときはTreeMap(キー順)かLinkedHashMap(追加順)を使います。
合計金額や在庫数の積み上げも同じ形で書けます。例えばtotal.merge(商品コード, 数量, Integer::sum)です。文字列の連結ならmap.merge(k, s, (a, b) -> a + "," + b)のように関数を変えるだけです。
グループ分けする(computeIfAbsentのコピペ例)
キーごとにリストへ追加する処理はcomputeIfAbsent(キー, k -> new ArrayList<>()).add(値)で書けます。 キーが無ければ空のリストを作って登録し、そのリストを返してくれるので、続けてaddできます。
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class Grouping {
record Task(String owner, String title) {}
public static void main(String[] args) {
List<Task> tasks = List.of(
new Task("佐藤", "見積書作成"),
new Task("鈴木", "備品発注"),
new Task("佐藤", "議事録整理"),
new Task("高橋", "在庫確認"),
new Task("鈴木", "会議室予約"));
// computeIfAbsent: キーが無ければ空リストを作ってから add
Map<String, List<String>> byOwner = new TreeMap<>();
for (Task t : tasks) {
byOwner.computeIfAbsent(t.owner(), k -> new ArrayList<>()).add(t.title());
}
System.out.println("computeIfAbsent : " + byOwner);
// Stream 版
Map<String, List<String>> byOwner2 = tasks.stream()
.collect(Collectors.groupingBy(Task::owner, TreeMap::new,
Collectors.mapping(Task::title, Collectors.toList())));
System.out.println("groupingBy : " + byOwner2);
// よくある書き間違い: putIfAbsent の戻り値は「前の値」なので初回は null
Map<String, List<String>> wrong = new TreeMap<>();
try {
wrong.putIfAbsent("佐藤", new ArrayList<>()).add("見積書作成");
} catch (NullPointerException e) {
System.out.println("putIfAbsent(...).add -> " + e.getClass().getSimpleName());
}
}
}
実行結果(Java 21.0.10):
computeIfAbsent : {佐藤=[見積書作成, 議事録整理], 鈴木=[備品発注, 会議室予約], 高橋=[在庫確認]}
groupingBy : {佐藤=[見積書作成, 議事録整理], 鈴木=[備品発注, 会議室予約], 高橋=[在庫確認]}
putIfAbsent(...).add -> NullPointerException
putIfAbsent(...).add(...)と書くと、初回は戻り値がnullなのでNullPointerExceptionになります。 putIfAbsentは「前の値」を返し、computeIfAbsentは「現在の値(今作ったものを含む)」を返す、という違いがそのまま出ています。グループ分けにはcomputeIfAbsentを使ってください。
merge・compute・putIfAbsentの使い分け
似たメソッドが多いので、「キーが無いとき」「キーがあるとき」に何をするかで整理します。
| メソッド | キーが無いとき | キーがあるとき | 戻り値 | 典型的な用途 |
|---|---|---|---|---|
put(k, v) |
追加 | 上書き | 前の値 | 普通の登録 |
putIfAbsent(k, v) |
追加 | 何もしない(値がnullなら上書き) |
前の値 | 初回だけ登録 |
computeIfAbsent(k, f) |
f(k)を追加 |
何もしない | 現在の値 | グループ分け・キャッシュ |
computeIfPresent(k, f) |
何もしない | f(k, v)で更新 |
更新後の値 | あるときだけ更新 |
compute(k, f) |
f(k, null)を追加 |
f(k, v)で更新 |
更新後の値 | 有無で分岐する計算 |
merge(k, v, f) |
vを追加 |
f(旧, v)で更新 |
更新後の値 | 集計(カウント・合計・連結) |
replace(k, v) |
何もしない | 上書き | 前の値 | あるときだけ置換 |
共通ルール:関数(f)がnullを返すと、そのキーは削除されます。 たとえば在庫を減らして0になったら消す処理は、次のように書けます。
// 在庫を1減らし、0になったらキーごと削除
stock.computeIfPresent("pen", (k, v) -> v - 1 == 0 ? null : v - 1);
compute系のラムダの中で、同じMapにputやremoveをしないでください。 HashMapのJavadocでは、関数の中でMapを変更した場合、ベストエフォートでConcurrentModificationExceptionを投げると説明されています。
ループ中に削除する(ConcurrentModificationExceptionの直し方)
拡張forでループしている最中にmap.remove(key)を呼ぶと、ConcurrentModificationExceptionが発生します。 削除はIterator.remove()かremoveIfで行います。在庫0の備品を消す例で、直す前と後を比べます。
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class CmeDemo {
static Map<String, Integer> sample() {
Map<String, Integer> m = new HashMap<>();
m.put("pen", 0);
m.put("note", 5);
m.put("clip", 0);
m.put("tape", 3);
return m;
}
public static void main(String[] args) {
// 直す前: 拡張for の中で map.remove
Map<String, Integer> m1 = sample();
try {
for (String key : m1.keySet()) {
if (m1.get(key) == 0) {
m1.remove(key);
}
}
} catch (Exception e) {
System.out.println("拡張for + remove : " + e);
}
// 直した後 1: Iterator.remove
Map<String, Integer> m2 = sample();
Iterator<Map.Entry<String, Integer>> it = m2.entrySet().iterator();
while (it.hasNext()) {
if (it.next().getValue() == 0) {
it.remove();
}
}
System.out.println("Iterator.remove : " + m2);
// 直した後 2: removeIf(Java 8+)
Map<String, Integer> m3 = sample();
m3.values().removeIf(v -> v == 0);
System.out.println("values().removeIf: " + m3);
Map<String, Integer> m4 = sample();
m4.entrySet().removeIf(e -> e.getKey().startsWith("p"));
System.out.println("entrySet().removeIf: " + m4);
// 値の更新は setValue なら OK
Map<String, Integer> m5 = sample();
for (Map.Entry<String, Integer> e : m5.entrySet()) {
e.setValue(e.getValue() + 1);
}
System.out.println("setValue : " + m5);
}
}
実行結果(Java 21.0.10):
拡張for + remove : java.util.ConcurrentModificationException
Iterator.remove : {note=5, tape=3}
values().removeIf: {note=5, tape=3}
entrySet().removeIf: {note=5, tape=3, clip=0}
setValue : {note=6, tape=4, pen=1, clip=1}
なぜ例外になるのか:HashMapは内部に「構造が変わった回数(modCount)」を持っていて、イテレータは作られた時点の回数を覚えています。ループ中にMap自身のremoveで件数が変わると回数がずれ、次のnext()で検知してConcurrentModificationExceptionを投げます(fail-fast)。イテレータ自身のremove()なら回数を合わせながら消すので例外になりません。
| やりたいこと | 書き方 | 備考 |
|---|---|---|
| 値の条件で削除 | map.values().removeIf(v -> 条件) |
一番短い |
| キーと値の両方で判定 | map.entrySet().removeIf(e -> 条件) |
|
| 削除しながら別の処理もしたい | Iterator+it.remove() |
ログ出力などを挟める |
| 値だけ更新 | entry.setValue(新しい値) または replaceAll |
件数が変わらないので例外にならない |
| ループ中にキーを追加 | 別のMapに集めて、ループ後にputAll |
List・Setでのループ中の削除は「Java List Set Map 使い分け」の記事で紹介しています。
HashMap・LinkedHashMap・TreeMap・ConcurrentHashMapの違い
Mapの実装クラスは「順序」「nullを入れられるか」「スレッド安全か」で選びます。 同じ4件を追加して、表示順とnullの扱いを比べました。
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ImplCompare {
static void fill(Map<String, Integer> m) {
m.put("delta", 4);
m.put("alpha", 1);
m.put("charlie", 3);
m.put("bravo", 2);
}
static String tryNull(Map<String, Integer> m) {
String k, v;
try { m.put(null, 0); k = "OK"; } catch (Exception e) { k = e.getClass().getSimpleName(); }
try { m.put("x", null); v = "OK"; } catch (Exception e) { v = e.getClass().getSimpleName(); }
return "nullキー=" + k + " / null値=" + v;
}
public static void main(String[] args) {
List<Map<String, Integer>> maps = List.of(
new HashMap<>(), new LinkedHashMap<>(), new TreeMap<>(), new ConcurrentHashMap<>());
for (Map<String, Integer> m : maps) {
fill(m);
System.out.printf("%-18s %s%n", m.getClass().getSimpleName(), m);
}
System.out.println();
for (Map<String, Integer> m : List.<Map<String, Integer>>of(
new HashMap<>(), new LinkedHashMap<>(), new TreeMap<>(), new ConcurrentHashMap<>())) {
System.out.printf("%-18s %s%n", m.getClass().getSimpleName(), tryNull(m));
}
}
}
実行結果(Java 21.0.10):
HashMap {bravo=2, alpha=1, delta=4, charlie=3}
LinkedHashMap {delta=4, alpha=1, charlie=3, bravo=2}
TreeMap {alpha=1, bravo=2, charlie=3, delta=4}
ConcurrentHashMap {bravo=2, alpha=1, delta=4, charlie=3}
HashMap nullキー=OK / null値=OK
LinkedHashMap nullキー=OK / null値=OK
TreeMap nullキー=NullPointerException / null値=OK
ConcurrentHashMap nullキー=NullPointerException / null値=NullPointerException
比較表(null列は上の実行結果、計算量・スレッド安全性は公式Javadocの記述によります):
| クラス | 順序 | nullキー | null値 | get・putの速さ |
スレッド安全 | 選ぶ場面 |
|---|---|---|---|---|---|---|
HashMap |
保証なし | 1つだけ可 | 可 | 平均O(1) | なし | 基本はこれ |
LinkedHashMap |
追加順(アクセス順も設定可) | 1つだけ可 | 可 | 平均O(1) | なし | 表示順を追加順にしたい、LRUキャッシュ |
TreeMap |
キーの昇順(Comparator指定可) |
不可 | 可 | O(log n) | なし | キー順の表示、範囲検索(floorEntry等) |
ConcurrentHashMap |
保証なし | 不可 | 不可 | 平均O(1) | あり | 複数スレッドからの読み書き |
Map.of/Map.copyOf |
保証なし | 不可 | 不可 | 平均O(1) | 変更不可なので安全に共有可 | 定数・設定値 |
複数スレッドでHashMapを更新するとどうなるか
HashMapを複数スレッドから同時に更新すると、更新が失われて数が合わなくなります。 8スレッドで10万回ずつmergeで数えた結果です。
import java.util.*;
import java.util.concurrent.*;
public class ThreadDemo {
static int run(Map<String, Integer> map) throws Exception {
try (ExecutorService pool = Executors.newFixedThreadPool(8)) {
for (int t = 0; t < 8; t++) {
pool.submit(() -> {
for (int i = 0; i < 100_000; i++) {
map.merge("count", 1, Integer::sum);
}
});
}
} // close() で全タスクの終了を待つ(Java 19+)
return map.get("count");
}
public static void main(String[] args) throws Exception {
System.out.println("期待値 : " + 8 * 100_000);
for (int i = 1; i <= 3; i++) {
System.out.println("HashMap 試行" + i + " : " + run(new HashMap<>()));
}
System.out.println("ConcurrentHashMap : " + run(new ConcurrentHashMap<>()));
}
}
実行結果(Java 21.0.10、2コアのマシン。HashMapの数値は実行のたびに変わります):
期待値 : 800000
HashMap 試行1 : 522060
HashMap 試行2 : 209671
HashMap 試行3 : 658176
ConcurrentHashMap : 800000
ConcurrentHashMapのmergeはキー単位で原子的に実行されるため、期待どおり80万になりました。 ただしConcurrentHashMapでもcontainsKeyしてからputのように2回に分けて呼ぶと、間に他のスレッドが割り込めます。「調べてから更新」はputIfAbsent・computeIfAbsent・mergeの1回の呼び出しにまとめます。
Map.ofで作るMapとHashMapの違い
Map.of(Java 9以降)は、変更できない(不変の)Mapを1行で作るメソッドです。 定数の対応表には便利ですが、putできない・null不可・キー重複で例外、という違いがあります。
import java.util.*;
public class MapOfDemo {
public static void main(String[] args) {
Map<String, Integer> fixed = Map.of("pen", 10, "note", 5);
System.out.println("Map.of : " + fixed.get("pen"));
try { fixed.put("clip", 1); } catch (Exception e) { System.out.println("put -> " + e); }
try { Map.of("pen", 1, "pen", 2); } catch (Exception e) { System.out.println("キー重複 -> " + e); }
try { Map.of("pen", null); } catch (Exception e) { System.out.println("null値 -> " + e); }
try { fixed.containsKey(null); } catch (Exception e) { System.out.println("containsKey(null) -> " + e); }
Map<String, Integer> editable = new HashMap<>(fixed);
editable.put("clip", 1);
System.out.println("new HashMap<>(Map.of(...)) で変更可: " + editable.containsKey("clip"));
Map<String, Integer> copy = Map.copyOf(editable);
System.out.println("Map.copyOf は変更不可: " + copy.getClass().getSimpleName());
Map<String, Integer> view = Collections.unmodifiableMap(editable);
editable.put("tape", 2);
System.out.println("unmodifiableMap は元の変更が見える: " + view.containsKey("tape"));
System.out.println("Map.copyOf はコピー時点のまま : " + copy.containsKey("tape"));
}
}
実行結果(Java 21.0.10):
Map.of : 10
put -> java.lang.UnsupportedOperationException
キー重複 -> java.lang.IllegalArgumentException: duplicate key: pen
null値 -> java.lang.NullPointerException
containsKey(null) -> java.lang.NullPointerException
new HashMap<>(Map.of(...)) で変更可: true
Map.copyOf は変更不可: MapN
unmodifiableMap は元の変更が見える: true
Map.copyOf はコピー時点のまま : false
| 作り方 | 変更 | null | キー重複 | 元の変更の反映 | 使いどころ |
|---|---|---|---|---|---|
new HashMap<>() |
できる | 可 | 上書き | ― | 普通に読み書きする |
Map.of(k1, v1, …) |
できない | 不可(containsKey(null)も例外) |
例外 | ― | 10組までの定数 |
Map.copyOf(map) |
できない | 不可 | ― | 反映されない(コピー) | 受け取った値を固定して保持 |
Collections.unmodifiableMap(map) |
できない | 元に従う | ― | 反映される(ビュー) | 読み取り専用で公開 |
初期値つきで変更もしたいときはnew HashMap<>(Map.of(...))と書くのが簡単です。 Map.ofは引数が10組(20個)までなので、それ以上はMap.ofEntriesを使います。なおMap.ofの表示順も保証されません。不変コレクション全般(List.of・Set.of)の話は「Java List Set Map 使い分け」の記事にまとめています。
Java 19〜21で増えたMap関連のメソッド
Java 19でHashMap.newHashMap(件数)、Java 21でLinkedHashMapなどに先頭・末尾を扱うメソッド(SequencedMap)が追加されました。 次の断片はimport java.util.*;をしたmainの中で実行しています。
Map<String, Integer> m = HashMap.newHashMap(100); // Java 19+: 100件入れても拡張しない容量で作る
LinkedHashMap<String, Integer> lm = new LinkedHashMap<>();
lm.put("b", 2);
lm.put("c", 3);
lm.putFirst("a", 1); // Java 21+
System.out.println(lm + " first=" + lm.firstEntry() + " last=" + lm.lastEntry());
System.out.println("reversed=" + lm.reversed() + " poll=" + lm.pollFirstEntry() + " -> " + lm);
実行結果(Java 21.0.10):
{a=1, b=2, c=3} first=a=1 last=c=3
reversed={c=3, b=2, a=1} poll=a=1 -> {b=2, c=3}
| メソッド | 追加された版 | 内容 |
|---|---|---|
HashMap.newHashMap(n) |
Java 19 | n件入れても容量の拡張が起きないサイズで作る(new HashMap<>(n)のnは容量なので意味が違う) |
putFirst/putLast |
Java 21 | 先頭・末尾に追加(LinkedHashMap) |
firstEntry/lastEntry/pollFirstEntry |
Java 21 | 先頭・末尾の取得・取り出し(SequencedMap) |
reversed() |
Java 21 | 逆順のビュー |
Java 25について:手元の環境はJava 21.0.10のため、Java 22〜25では実行していません。公式のAPI差分一覧(Java SE 25の「New API」)を確認したところ、HashMap・Map・LinkedHashMap・TreeMap・ConcurrentHashMapに追加されたメソッドはありませんでした。この記事のコードはJava 25でもそのまま使える書き方です。
よくあるつまずきと直し方のチェックリスト
| 症状・エラー | 原因 | 直し方 |
|---|---|---|
getがnullを返す |
キーが無い/値がnull/キーのequals未実装 |
containsKeyで確認、getOrDefault、キーをrecordに |
int x = map.get(k);でNullPointerException |
nullをintへボクシング解除 |
map.getOrDefault(k, 0) |
putIfAbsent(...).add(...)でNullPointerException |
初回の戻り値がnull |
computeIfAbsent(k, x -> new ArrayList<>()).add(...) |
ループ中のremoveでConcurrentModificationException |
fail-fastイテレータが変更を検知 | removeIfかIterator.remove() |
Map.ofにputしてUnsupportedOperationException |
不変のMap |
new HashMap<>(Map.of(...)) |
| 複数スレッドで数が合わない | HashMapは同時更新に非対応 |
ConcurrentHashMap+merge |
よくある質問
Q. HashMapとMapは何が違いますか?
A. Mapはインターフェース(約束ごと)で、HashMapはその実装クラスです。 変数はMap<String, Integer> m = new HashMap<>();とMap型で受けておくと、後からLinkedHashMapやTreeMapに替えやすくなります。
Q. getOrDefaultの既定値はMapに登録されますか?
A. 登録されません。 getOrDefaultは取り出すだけでMapを変更しないので、size()も変わりません。登録もしたい場合はcomputeIfAbsentかputIfAbsentを使います。
Q. HashMapをループするとき、一番よい方法はどれですか?
A. キーと値を両方使うならentrySetの拡張for、短く書くならforEachです。 途中でbreakしたい場合は拡張for、絞り込みや並べ替えをしたい場合はStreamが向いています。100万件の簡易計測では、どれも数十ミリ秒で大きな差はありませんでした。
まとめ
HashMapはキーで値をすばやく引くクラス。型はMapで受け、キーは重複不可、順序は保証されない- 無いキーの既定値は
getOrDefault。ただし値がnullで登録済みならnullが返る - カウント・合計は
merge(k, 1, Integer::sum)、グループ分けはcomputeIfAbsent(k, x -> new ArrayList<>()).add(v) - ループは
entrySetの拡張forかforEach。Streamは絞り込み・並べ替え向き - ループ中の削除は
removeIfかIterator.remove()でConcurrentModificationExceptionを避ける - 順序なら
LinkedHashMap/TreeMap、複数スレッドならConcurrentHashMap、定数はMap.of - Java 21.0.10で全コードを実行して確認。Java 22〜25でMap関連のAPI追加はない(公式のAPI差分で確認)
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Java HashMapの使い方|ループ・getOrDefault・merge早見表」
株式会社RJCでは、HashMapのような毎日使う道具の特徴を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。