MultiMap は「1つのキーに“複数の値”をぶら下げる Map」
普通の Map<K, V> は「キー1つにつき値1つ」です。
でも業務では、「キー1つに対して値が複数ある」場面がよく出てきます。
部署 → 所属メンバー一覧
タグ → そのタグが付いた記事一覧
ユーザーID → そのユーザーが持つ権限一覧
こういうときに欲しくなるのが MultiMap(マルチマップ)、
つまり「K -> List<V> をいい感じに扱うための仕組み」です。
Java標準には MultiMap はありませんが、
「Map<K, List<V>> をラップしたユーティリティ」を自作すれば、
実務でも十分使える形になります。
一番シンプルな形:Map<K, List<V>> をラップする
まずは「使いやすい API」を決める
「MultiMap をどう実装するか」の前に、
「どう使いたいか」を先に決めると設計が楽になります。
例えば、こんな感じで使えたら嬉しいはずです。
MultiMap<String, String> mm = new MultiMap<>();
mm.put("sales", "田中");
mm.put("sales", "佐藤");
mm.put("dev", "鈴木");
System.out.println(mm.get("sales")); // [田中, 佐藤]
System.out.println(mm.get("dev")); // [鈴木]
System.out.println(mm.get("hr")); // [](空のリスト)
Javaこのイメージをそのままコードに落としていきます。
基本実装:MultiMap クラスを自作する
コアは「Map<K, List<V>>」+「値追加のユーティリティ」
import java.util.*;
public class MultiMap<K, V> {
private final Map<K, List<V>> map = new HashMap<>();
public void put(K key, V value) {
List<V> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(value);
}
public List<V> get(K key) {
List<V> list = map.get(key);
if (list == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(list);
}
public boolean remove(K key, V value) {
List<V> list = map.get(key);
if (list == null) {
return false;
}
boolean removed = list.remove(value);
if (list.isEmpty()) {
map.remove(key);
}
return removed;
}
public void removeAll(K key) {
map.remove(key);
}
public Set<K> keySet() {
return map.keySet();
}
public boolean isEmpty() {
return map.isEmpty();
}
}
Javaここから、重要なポイントを丁寧に分解していきます。
重要ポイント1:put で「List の生成」と「追加」を一箇所に閉じ込める
呼び出し側に「List の存在」を意識させない
put の中身を見てください。
public void put(K key, V value) {
List<V> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(value);
}
Javaここでやっていることは、
- そのキーの List があるか調べる
- なければ新しく作って Map に入れる
- List に値を追加する
というお決まりパターンです。
これを毎回呼び出し側で書くと、こうなります。
List<V> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(value);
Javaこれがあちこちに散らばると、
コピペだらけでバグの温床になります。
「List の生成と追加のロジックを MultiMap の中に閉じ込める」
これが一番大事な設計ポイントです。
呼び出し側は「put(key, value) するだけ」で済むようにしておくと、
コードがかなりスッキリします。
重要ポイント2:get で「空のリスト」を返すか「null」を返すか
呼び出し側のコードをどうしたいかで決める
get の実装をもう一度見ます。
public List<V> get(K key) {
List<V> list = map.get(key);
if (list == null) {
return Collections.emptyList();
}
return Collections.unmodifiableList(list);
}
Javaここでのポイントは二つです。
一つ目は、「キーが存在しないときに null ではなく“空のリスト”を返している」ことです。
これにより、呼び出し側はこう書けます。
for (V v : mm.get(key)) {
...
}
Javanull チェックが不要になり、
「値がない=空のリスト」という自然な扱いになります。
二つ目は、「中身の List をそのまま返さず、unmodifiableList でラップしている」ことです。
これにより、呼び出し側が get(key).add(...) で勝手に中身をいじるのを防げます。
MultiMap の中で「どう保持するか」を管理したいので、
外からは「読み取り専用ビュー」だけ渡す、という設計が安全です。
重要ポイント3:remove で「空になったキーを消す」
Map の中に「空の List」を残さない工夫
remove の実装を見ます。
public boolean remove(K key, V value) {
List<V> list = map.get(key);
if (list == null) {
return false;
}
boolean removed = list.remove(value);
if (list.isEmpty()) {
map.remove(key);
}
return removed;
}
Javaここでのポイントは、
「最後の1件を消したら、そのキー自体も Map から消している」
ということです。
これをしないと、
「値は空だけどキーだけ残っている」状態が増えていきます。
業務で長く動くプロセスでは、
こういう「ゴミのようなエントリ」が積もると、
メモリやパフォーマンスにじわじわ効いてきます。
「値が空になったキーは消す」というルールを MultiMap の中に閉じ込めておくと、
呼び出し側は何も意識せずに済みます。
具体例1:部署 → メンバー一覧の MultiMap
「1部署に複数メンバー」が自然に書ける
先ほどの MultiMap<String, String> を使って、
部署とメンバーの対応を管理してみます。
public class DepartmentExample {
public static void main(String[] args) {
MultiMap<String, String> members = new MultiMap<>();
members.put("営業", "田中");
members.put("営業", "佐藤");
members.put("開発", "鈴木");
System.out.println("営業: " + members.get("営業")); // [田中, 佐藤]
System.out.println("開発: " + members.get("開発")); // [鈴木]
System.out.println("総務: " + members.get("総務")); // []
}
}
Javaここでの重要ポイントは、
「呼び出し側は“Map of List”を意識せず、“MultiMap”として扱えている」
ということです。
put も get も、普通の Map とほぼ同じ感覚で書けます。
内部で List をどう管理するかは、MultiMap に任せてしまえます。
具体例2:タグ → 記事ID一覧の MultiMap
「1記事に複数タグ」「1タグに複数記事」の典型パターン
ブログやナレッジベースなどでよくある「タグ付け」の例です。
public class TagIndex {
private final MultiMap<String, Long> index = new MultiMap<>();
public void addTag(long articleId, String tag) {
index.put(tag, articleId);
}
public List<Long> findArticlesByTag(String tag) {
return index.get(tag);
}
}
Java使い方のイメージです。
TagIndex tagIndex = new TagIndex();
tagIndex.addTag(1L, "Java");
tagIndex.addTag(1L, "入門");
tagIndex.addTag(2L, "Java");
tagIndex.addTag(3L, "Spring");
System.out.println(tagIndex.findArticlesByTag("Java")); // [1, 2]
System.out.println(tagIndex.findArticlesByTag("入門")); // [1]
System.out.println(tagIndex.findArticlesByTag("SQL")); // []
Javaここでのポイントは、
「MultiMap を使うことで、“タグ→記事ID一覧”という構造が素直に表現できている」
ということです。
もし MultiMap がなければ、Map<String, List<Long>> に対して毎回「List の有無チェック+生成+追加」を書くことになります。
もう一歩進める:computeIfAbsent で実装をスッキリさせる
Java 8 以降なら、Map のユーティリティも活用できる
さきほどの put は、computeIfAbsent を使うともっと短く書けます。
public void put(K key, V value) {
List<V> list = map.computeIfAbsent(key, k -> new ArrayList<>());
list.add(value);
}
JavacomputeIfAbsent は、
「キーがなければラムダで値を作って入れ、あればそのまま返す」
という便利メソッドです。
これを使うと、
キーがあるかチェック
なければ List を作って put
List に add
という一連の流れを、
かなりコンパクトに表現できます。
ここでの重要ポイントは、
「MultiMap の中でも、Map のユーティリティメソッドを活用すると、実装がシンプルになる」
ということです。
まとめ:MultiMap実装で身につけてほしい感覚
MultiMap は、
単に「Map<K, List<V>> の別名」ではなく、
「“1キーに複数値”というパターンを、呼び出し側から隠してあげるユーティリティ」
です。
put の中に「List の生成+追加」のお決まりパターンを閉じ込める。get では null ではなく「空のリスト」を返し、呼び出し側のコードをシンプルにする。remove では「最後の1件を消したらキーごと消す」ことで、Map をきれいに保つ。
タグ、部署、権限、インデックスなど、「1対多」の関係を扱うところに素直に当てはめる。
あなたのコードのどこかに、Map<K, List<V>> に対して毎回同じような「List の有無チェック+生成+追加」を書いている箇所があれば、
そこを一度「MultiMap クラス」に切り出せないか眺めてみてください。
その小さな抽象化が、
「よく出るパターンを“名前のついた道具”にしていけるエンジニア」への、
いい一歩になります。
