0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaのcomputeIfAbsentとmergeの違い:Mapの初期化・集計でハマらない実装

0
Last updated at Posted at 2026-10-20

Javaで「キーごとに値をまとめる」「同じキーが来たらカウントを増やす」という処理を書くとき、毎回 containsKey で分岐していませんか。

Map(キーと値を対応付けて保存する入れ物)には、値がないときだけ作る computeIfAbsent と、既存の値に新しい値を統合する merge が用意されています。どちらも便利ですが、null の扱いと並行処理には注意が必要です。

結論:何をしたいかで選ぶ

やりたいこと 選ぶメソッド 典型例
キーがない場合に初期値を作る computeIfAbsent キーごとに List を作る
既存値へ値を足す・結合する merge URLごとのアクセス件数を数える
存在する値も含めて再計算する compute カウンターの増減など
値を読むだけ getOrDefault キーがなければ0を返す

ここでの「キーがない」は厳密には「キーに対応する値が null」の場合も含みます。HashMap は null 値を許可しますが、ConcurrentHashMap は許可しません。

1. computeIfAbsent:必要なときだけListを生成する

ユーザーを部署別にまとめる場面を考えます。

Map<String, List<String>> members = new HashMap<>();

members.computeIfAbsent("backend", key -> new ArrayList<>()).add("佐藤");
members.computeIfAbsent("backend", key -> new ArrayList<>()).add("田中");
members.computeIfAbsent("frontend", key -> new ArrayList<>()).add("鈴木");

System.out.println(members.get("backend")); // [佐藤, 田中]

computeIfAbsent(key, mappingFunction) は、値がなければ mappingFunction(値を作る関数)を実行し、結果を登録して返します。すでに値があれば、作成関数を実行せずその値を返します。

上の例では backend の ArrayList は初回だけ生成され、2回目は同じリストに追加されます。key はラムダ式(-> で書く短い関数)が受け取る引数です。使わないなら ignored などの名前でも構いません。

従来の get → put の分岐より「存在しなければ作る」という意図が読みやすくなります。

ハマりどころ:作成関数がnullを返す場合

Map<String, String> cache = new HashMap<>();
String value = cache.computeIfAbsent("missing", key -> null);

System.out.println(value);                    // null
System.out.println(cache.containsKey("missing")); // false

作成関数が null を返すとエントリーは追加されません。次回も作成関数が呼ばれます。「見つからなかったこと」もキャッシュしたい場合は、Optional や別の状態表現を検討します。

2. merge:キー単位のカウントアップに向く

Map<String, Integer> requests = new HashMap<>();
for (String path : List.of("/orders", "/users", "/orders")) {
    requests.merge(path, 1, Integer::sum);
}

System.out.println(requests.get("/orders")); // 2
System.out.println(requests.get("/users"));  // 1

merge(key, value, remappingFunction) は、値がなければ value をそのまま登録します。既存の値があれば remappingFunction(古い値と追加値を合成する関数)で新しい値を決めます。

Integer::sum はメソッド参照で、ここでは (oldValue, addedValue) -> oldValue + addedValue と同じ意味です。初回は1を保存し、2回目以降は既存値に1を足すため、アクセス件数の集計などに向きます。

ハマりどころ:統合関数がnullを返すと削除される

Map<String, Integer> points = new HashMap<>();
points.put("user1", 10);

points.merge("user1", 1, (oldValue, addedValue) -> null);
System.out.println(points.containsKey("user1")); // false

merge の第2引数の値はnullにできません。一方、第3引数の関数が null を返した場合は、登録済みのキーが削除されます。「計算できなかったからnullを返す」のつもりで書くと、データが消える可能性があります。

3. 並行処理ではHashMapのまま使わない

ここまでの HashMap は、1つのスレッドで処理する例です。スレッドはプログラムの処理の実行単位です。複数のHTTPリクエストが同じ HashMap を更新するときは、computeIfAbsent や merge を使うだけではスレッド安全になりません。

Map インターフェースの標準実装は、これらのメソッドに対して原子性(同時操作に途中で割り込まれない性質)を保証していません。共有データなら用途に応じて ConcurrentHashMap などを使います。

ConcurrentHashMap<String, LongAdder> counts = new ConcurrentHashMap<>();
counts.computeIfAbsent("/orders", ignored -> new LongAdder()).increment();
counts.computeIfAbsent("/orders", ignored -> new LongAdder()).increment();

System.out.println(counts.get("/orders").sum()); // 2

LongAdder は複数スレッドからの加算に向いたカウンターです。この組み合わせは ConcurrentHashMap の公式APIドキュメントでも紹介されています。ただし、更新が続いている最中の sum() は厳密な一瞬のスナップショットではありません。課金の確定件数など、完全な整合性が必要な処理はDBトランザクションなど別の仕組みで保証しましょう。

また、ConcurrentHashMap が安全に管理するのはMapの登録・更新です。値に格納した通常の ArrayList への並行 add まで安全にするわけではありません。キー別リストを共有するときは、値側の同期方法も設計してください。

4. ラムダ式の中でMap自体を更新しない

// NG例:計算中に同じMapを更新する
map.computeIfAbsent("a", key -> {
    map.put("b", "value");
    return "created";
});

computeIfAbsent や merge に渡す関数から、同じMapへ別の更新を加えないようにします。実装によって例外になったり、並行更新時に問題が起きたりします。

特に ConcurrentHashMap.computeIfAbsent の作成関数は、短く単純な処理にすることが公式に推奨されています。重いDB問い合わせや外部API通信をその中で実行すると、他スレッドのMap更新を待たせる場合があります。リトライ、期限切れ(TTL)、失敗時の再試行などが必要なキャッシュは、専用のキャッシュライブラリも検討しましょう。

5. 動作確認できる最小サンプル

MapUpdateTips.java というファイルに次を保存して実行できます。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

public class MapUpdateTips {
    public static void main(String[] args) {
        Map<String, List<String>> byTeam = new HashMap<>();
        byTeam.computeIfAbsent("backend", ignored -> new ArrayList<>()).add("佐藤");
        byTeam.computeIfAbsent("backend", ignored -> new ArrayList<>()).add("田中");
        if (!byTeam.get("backend").equals(List.of("佐藤", "田中"))) {
            throw new AssertionError("grouping failed");
        }

        Map<String, Integer> requests = new HashMap<>();
        for (String path : List.of("/orders", "/users", "/orders")) {
            requests.merge(path, 1, Integer::sum);
        }
        if (!Integer.valueOf(2).equals(requests.get("/orders"))) {
            throw new AssertionError("counting failed");
        }

        ConcurrentHashMap<String, LongAdder> concurrent = new ConcurrentHashMap<>();
        concurrent.computeIfAbsent("/orders", ignored -> new LongAdder()).increment();
        if (concurrent.get("/orders").sum() != 1L) {
            throw new AssertionError("concurrent count failed");
        }
        System.out.println("OK: grouping, merge and counter");
    }
}
javac MapUpdateTips.java
java MapUpdateTips

javac はJavaソースを .class ファイルにコンパイルするコマンド、java はコンパイルしたクラスを実行するコマンドです。この例はJava 9以降の List.of を使用しています(computeIfAbsent と merge 自体はJava 8から)。

実行結果: OK: grouping, merge and counter

まとめ

  • computeIfAbsent は「存在しなければ初期化」。List のグループ分けや遅延初期化に使う。
  • merge は「値を合成」。集計に便利だが、統合関数の null は削除を意味する。
  • 並行アクセスがあるならMap自体だけでなく、値の可変オブジェクトも安全性を検討する。
  • Mapの更新関数には重い処理や同じMapへの再更新を入れない。

参考資料(2026年10月17日確認)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?