Java Tips | コレクション:MultiMap実装

Java Java
スポンサーリンク

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

ここでやっていることは、

  1. そのキーの List があるか調べる
  2. なければ新しく作って Map に入れる
  3. 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)) {
    ...
}
Java

null チェックが不要になり、
「値がない=空のリスト」という自然な扱いになります。

二つ目は、「中身の 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”として扱えている」
ということです。

putget も、普通の 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);
}
Java

computeIfAbsent は、
「キーがなければラムダで値を作って入れ、あればそのまま返す」
という便利メソッドです。

これを使うと、

キーがあるかチェック
なければ 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 クラス」に切り出せないか眺めてみてください。

その小さな抽象化が、
「よく出るパターンを“名前のついた道具”にしていけるエンジニア」への、
いい一歩になります。

タイトルとURLをコピーしました