Java Tips | コレクション:Map反転

Java Java
スポンサーリンク

Map反転は「矢印の向きをひっくり返す」技

Map<K, V> は「K → V」という矢印の集まりです。
Map反転は、この矢印の向きを「V → K」にひっくり返して、新しい Map<V, K> を作るイメージです。

コード → ラベル(Integer → String)の Map があるとき、
ラベル → コード(String → Integer)の Map も欲しくなることがあります。

この「片方向の対応表から、逆向きの対応表を作る」のが Map反転です。
業務では「マスタの逆引き」「画面表示値からコードを引きたい」といった場面でよく使います。


一番基本:単純な Map を反転する

K → V を V → K にするシンプルな実装

まずは、キーも値もユニークで、重複がない前提の一番シンプルな反転から。

import java.util.HashMap;
import java.util.Map;

public final class Maps {

    private Maps() {}

    public static <K, V> Map<V, K> invert(Map<K, V> source) {
        Map<V, K> result = new HashMap<>();
        if (source == null || source.isEmpty()) {
            return result;
        }
        for (Map.Entry<K, V> e : source.entrySet()) {
            result.put(e.getValue(), e.getKey());
        }
        return result;
    }
}
Java

使い方はこうなります。

Map<Integer, String> codeToLabel = Map.of(
        1, "有効",
        2, "無効"
);

Map<String, Integer> labelToCode = Maps.invert(codeToLabel);

// "有効" → 1, "無効" → 2 で引ける
System.out.println(labelToCode.get("有効")); // 1
Java

ここでの重要ポイントは二つです。

一つ目は、「result.put(e.getValue(), e.getKey()) で“キーと値を入れ替えている”」ことです。
K → V を一つずつ見て、V → K として新しい Map に詰め直しています。

二つ目は、「元の Map は一切変更せず、新しい Map を作っている」ことです。
反転は“別の見方の対応表”を作るイメージなので、元の表はそのまま残しておくのが基本です。


重要ポイント:値が重複しているとどうなるか

「値がユニークでない Map は、きれいには反転できない」

ここが Map反転で一番大事なポイントです。

元の Map がこうだったとします。

Map<Integer, String> codeToLabel = Map.of(
        1, "有効",
        2, "無効",
        3, "有効"
);
Java

"有効" が 1 と 3 の二つのキーに対応しています。
これを単純に反転すると、「"有効" → 1"有効" → 3 のどちらを採用するのか?」という問題が発生します。

先ほどの invert 実装だと、後から put した方で上書きされます。
つまり、結果は "有効" → 3 だけが残り、1 は消えます。

業務的にこれは危険です。
「どちらかが勝手に消える」仕様は、バグの温床になります。

だからこそ、Map反転を使うときは必ず次のどちらかを決める必要があります。

値は必ずユニークである(重複しない)と前提にする。
値が重複する場合は、「反転先を Map<V, List<K>> にする」など、複数キーを受け止められる形にする。


安全な反転:値の重複をチェックして例外にする

「値はユニークであるべき」前提をコードに刻む

「この Map は絶対に値が重複しない」という前提で使うなら、
重複を見つけたときに例外を投げる実装にしておくと安全です。

import java.util.HashMap;
import java.util.Map;

public final class Maps {

    private Maps() {}

    public static <K, V> Map<V, K> invertStrict(Map<K, V> source) {
        Map<V, K> result = new HashMap<>();
        if (source == null || source.isEmpty()) {
            return result;
        }
        for (Map.Entry<K, V> e : source.entrySet()) {
            V value = e.getValue();
            if (result.containsKey(value)) {
                throw new IllegalArgumentException(
                        "値が重複しています: " + value);
            }
            result.put(value, e.getKey());
        }
        return result;
    }
}
Java

ここでの重要ポイントは、
「“値はユニークであるべき”という業務ルールを、コードで検証している」ことです。

もしマスタ定義のミスなどで値が重複していたら、
静かに上書きされるのではなく、すぐに例外で気づけます。

「この Map は反転して使う前提だから、値の重複は許さない」という意図を、
invertStrict という名前と例外で明確に表現しています。


値が重複する場合の反転:Map<V, List<K>> にする

「同じ値にぶら下がるキーを全部取りたい」パターン

「値が重複することは分かっているし、それを活かしたい」というケースもあります。
たとえば「ステータス → そのステータスを持つユーザーID一覧」のような逆引きです。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public final class Maps {

    private Maps() {}

    public static <K, V> Map<V, List<K>> invertToList(Map<K, V> source) {
        Map<V, List<K>> result = new HashMap<>();
        if (source == null || source.isEmpty()) {
            return result;
        }
        for (Map.Entry<K, V> e : source.entrySet()) {
            K key = e.getKey();
            V value = e.getValue();
            result.computeIfAbsent(value, v -> new ArrayList<>())
                  .add(key);
        }
        return result;
    }
}
Java

使い方の例です。

Map<String, String> userToStatus = Map.of(
        "u001", "ACTIVE",
        "u002", "INACTIVE",
        "u003", "ACTIVE"
);

Map<String, List<String>> statusToUsers =
        Maps.invertToList(userToStatus);

// "ACTIVE" → ["u001", "u003"]
// "INACTIVE" → ["u002"]
Java

ここで深掘りしたい重要ポイントは三つです。

一つ目は、「反転先の型を Map<V, List<K>> にすることで、“同じ値にぶら下がる複数キー”を全部保持できている」ことです。

二つ目は、「computeIfAbsent を使って、“値ごとのリストを初期化しつつ追加する”処理を簡潔に書いている」ことです。
result.computeIfAbsent(value, v -> new ArrayList<>()).add(key); という一行で、「なければ作る+追加する」が表現できています。

三つ目は、「“値が重複することを前提にした反転”と、“値はユニークであるべき反転”を別メソッドに分けている」ことです。
これにより、呼び出し側は用途に応じて明示的に選べます。


Stream を使った Map反転

entrySet → collect の基本パターン

Stream を使うと、Map反転は次のようにも書けます。

import java.util.Map;
import java.util.stream.Collectors;

Map<Integer, String> codeToLabel = Map.of(
        1, "有効",
        2, "無効"
);

Map<String, Integer> labelToCode =
        codeToLabel.entrySet().stream()
                   .collect(Collectors.toMap(
                           Map.Entry::getValue,
                           Map.Entry::getKey
                   ));
Java

ここでの重要ポイントは、
Collectors.toMap(キー関数, 値関数)getValuegetKey を渡すことで、“反転”をそのまま表現している」ことです。

ただし、この toMap 版も「値が重複していると例外になる」ことに注意が必要です。
IllegalStateException: Duplicate key が投げられます)

「値の重複を許容したい」場合は、toMap の第三引数(マージ関数)を指定するか、
先ほどの invertToList のように Map<V, List<K>> を自前で組み立てる方が分かりやすいです。


まとめ:Map反転ユーティリティで身につけてほしい感覚

Map反転は、
単に「キーと値を入れ替えるテクニック」ではなく、
「片方向の対応表を、逆方向からも引けるように設計する技術」です。

invert のような単純反転は、「値がユニークである」前提がないと危険になる。
「値はユニークであるべき」なら、invertStrict のように重複検出して例外にする。
「値の重複を活かしたい」なら、Map<V, List<K>> に反転する invertToList を使う。
Stream では entrySet().stream()Collectors.toMap(getValue, getKey) という形で反転を表現できるが、重複時の挙動を意識する。

あなたのコードのどこかに、
「手作業で“ラベル → コード”の Map をもう一つ書いている」箇所があれば、
それを一度「元の Map からの反転ユーティリティ」に置き換えられないか眺めてみてください。

その小さな整理が、
「対応表を一元管理しつつ、両方向からスマートに引けるエンジニア」への、
確かな一歩になります。

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