Map.computeIfAbsent / compute / merge — 集計ロジック簡潔化
「存在チェック→取得→更新→再格納」の冗長なコードを、1行の原子操作で置き換えるのが computeIfAbsent、compute、merge。初心者でも使い分けられるよう、目的別に整理します。
役割の違いと使いどころ
- 目的別の選択:
- computeIfAbsent: キーが「ないときだけ」初期値を作って入れる(遅延初期化・オンデマンド作成)。
- compute: 既存値を使って「計算して置き換える」。存在しない場合の初期値も含めて「一括で計算」。
- merge: 「新しい値」と「既存値」を関数で合成する(集計・加算・結合に最適)。
- 共通の強み: 一度の呼び出しで読み取り→計算→書き込みを原子的に行うため、分割記述のバグや競合を減らす。
computeIfAbsent(ないときだけ作る)
典型パターン(Map<Key, List<Value>>)
Map<String, List<String>> studentsByClass = new HashMap<>();
void addStudent(String classRoom, String name) {
studentsByClass
.computeIfAbsent(classRoom, k -> new ArrayList<>()) // ないときだけ新規リスト
.add(name); // そのまま追加
}
Java- ラベル:
- 遅延初期化: 高コストなオブジェクト生成を「必要な時だけ」。
- 多値Map: 値がコレクションのときに最も便利。
- シンプル化:
containsKey→get→putを不要にする。
compute(計算して置き換える)
既存値を更新、なければ初期化
Map<String, Integer> stock = new HashMap<>();
void addStock(String sku, int add) {
stock.compute(sku, (k, v) -> (v == null) ? add : v + add);
}
Java- ラベル:
- 柔軟: 「存在する/しない」の分岐を同じラムダで記述。
- 削除もできる: 計算結果が
nullなら、そのキーは削除される。
// 在庫が0以下になったら削除
stock.compute(sku, (k, v) -> {
int newVal = (v == null ? 0 : v) - 1;
return newVal <= 0 ? null : newVal; // null → エントリ削除
});
Javamerge(既存値と新値を合成)
カウンタ加算
Map<String, Integer> counts = new HashMap<>();
void inc(String word) {
counts.merge(word, 1, Integer::sum); // ない場合は1、ある場合は足し込み
}
Java文字列の結合・集合のマージ
Map<String, String> joined = new HashMap<>();
joined.merge("A", "foo", (old, x) -> old + "," + x); // "foo"
joined.merge("A", "bar", (old, x) -> old + "," + x); // "foo,bar"
Map<String, Set<String>> tags = new HashMap<>();
tags.merge("post1", new HashSet<>(Set.of("java")),
(old, add) -> { old.addAll(add); return old; });
Java- ラベル:
- 集計: 加算、最大/最小、結合などの「畳み込み」に最適。
- 初期化不要: キーがなければそのまま新値が入る。
例題で理解する
例題1: 多段マップ(Map<Dept, Map<Day, List<Task>>>)
Map<String, Map<Integer, List<String>>> schedule = new HashMap<>();
void addTask(String dept, int day, String task) {
schedule
.computeIfAbsent(dept, d -> new HashMap<>())
.computeIfAbsent(day, d -> new ArrayList<>())
.add(task);
}
Java- ポイント: ネストでも「ないときだけ作る」連鎖でスッキリ。
例題2: トップNランキングの集計(merge)
Map<String, Integer> sales = new HashMap<>();
void addSale(String user, int amount) {
sales.merge(user, amount, Integer::sum);
}
// 集計後は値でソートして上位表示(例)
List<Map.Entry<String,Integer>> top = new ArrayList<>(sales.entrySet());
top.sort(Map.Entry.<String,Integer>comparingByValue().reversed());
Java例題3: 状態遷移(compute)
enum State { NEW, IN_PROGRESS, DONE }
Map<String, State> ticket = new HashMap<>();
void advance(String id) {
ticket.compute(id, (k, s) -> {
if (s == null || s == State.NEW) return State.IN_PROGRESS;
if (s == State.IN_PROGRESS) return State.DONE;
return State.DONE;
});
}
Javaよくある落とし穴と回避策
- 落とし穴: 値がコレクションのとき、
mergeで「新しいインスタンスを返さず、共有参照を誤って破壊」。- 回避: 共有参照を理解した上で「同じインスタンスに add」するか、必要に応じて防御的コピー。
- 落とし穴:
computeのラムダが副作用で例外を投げ、部分状態が中途半端。- 回避: 計算ラムダは短く安全に。入出力の外部I/Oを避ける。
- 落とし穴:
computeIfAbsentの初期化が高コストで競合が多い。- 回避: 初期化関数を軽量に/後続で非同期初期化に分離。
- 落とし穴:
nullの扱いを誤解(compute結果がnullなら削除)。- 回避: 「削除」の意図がないなら
nullを返さない。
- 回避: 「削除」の意図がないなら
テンプレート集
- ないときだけ初期化して使う
V v = map.computeIfAbsent(key, k -> createInitial(k));
Java- 存在/非存在の両方を一括で計算
map.compute(key, (k, old) -> old == null ? init(k) : update(old));
Java- 集計・結合(初期化込み)
map.merge(key, newVal, (old, x) -> combine(old, x));
Java- コレクション値の多値Map
map.computeIfAbsent(key, k -> new ArrayList<>()).add(val);
Java- 削除条件付き更新
map.compute(key, (k, v) -> shouldRemove(v) ? null : transform(v));
Java並行環境の補足(簡潔に安全に)
- 単一Mapを多数スレッドで更新:
ConcurrentHashMapと組み合わせると、同じメソッドが原子的に働く。 - 高頻度の加算:
merge(key, 1, Integer::sum)でも良いが、超高競合ではLongAdder併用が有利。 - 一貫したキー設計: キーは不変(
equals/hashCodeが安定)に。
まとめ
- 「存在チェック+更新」を1行にするのが
computeIfAbsent/compute/merge。 - 多値Mapは
computeIfAbsent、集計はmerge、複雑な更新や削除条件付きはcomputeが得意。 - シンプルさと原子性でバグを減らし、読みやすい集計コードにできる。
