Java 逆引き集 | Map.computeIfAbsent / compute / merge — 集計ロジック簡潔化

Java Java
スポンサーリンク

Map.computeIfAbsent / compute / merge — 集計ロジック簡潔化

「存在チェック→取得→更新→再格納」の冗長なコードを、1行の原子操作で置き換えるのが computeIfAbsentcomputemerge。初心者でも使い分けられるよう、目的別に整理します。


役割の違いと使いどころ

  • 目的別の選択:
    • 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: 値がコレクションのときに最も便利。
    • シンプル化: containsKeygetput を不要にする。

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 → エントリ削除
});
Java

merge(既存値と新値を合成)

カウンタ加算

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行にするのが computeIfAbsentcomputemerge
  • 多値Mapは computeIfAbsent、集計は merge、複雑な更新や削除条件付きは compute が得意。
  • シンプルさと原子性でバグを減らし、読みやすい集計コードにできる。
タイトルとURLをコピーしました