Java Tips | コレクション:不変Map生成

Java Java
スポンサーリンク

不変Map生成は「絶対に変わらない対応表」をコードで保証する技術

不変Map(Immutable Map)は、「一度作ったらキーと値の対応が二度と変わらないMap」です。
putremoveclear もできません。

業務システムでは、次のような「変わったら困る対応表」がたくさんあります。

画面コード → ラベル
ステータスコード → 表示名
権限コード → 説明文

こういったものを「なんとなく HashMap で持つ」と、
どこかで put されてしまい、気づかないうちに中身が変わる危険があります。

不変Map生成は、「ここは絶対に変わらない」という意図を、
コードと型でしっかり表現するための技術です。


Java標準での不変Mapの作り方

Java 9 以降:Map.of(...) で一発生成

Java 9 以降なら、不変Mapは Map.of(...) で簡単に作れます。

import java.util.Map;

public class ImmutableMapSample {

    public static void main(String[] args) {
        Map<String, String> statusLabels = Map.of(
                "NEW", "新規",
                "IN_PROGRESS", "処理中",
                "DONE", "完了"
        );

        System.out.println(statusLabels.get("NEW")); // 新規

        statusLabels.put("CANCEL", "キャンセル"); // UnsupportedOperationException
    }
}
Java

ここで押さえておきたい重要ポイントは二つです。

一つ目は、「見た目は普通の Map だが、変更操作はすべて例外になる」ということです。
putremoveclear などはすべて UnsupportedOperationException になります。

二つ目は、「Map.of で作ったMapを定数として公開すると、“変えちゃダメな対応表”だと一目で分かる」ことです。

public final class Statuses {

    private Statuses() {}

    public static final Map<String, String> LABELS =
            Map.of(
                    "NEW", "新規",
                    "IN_PROGRESS", "処理中",
                    "DONE", "完了"
            );
}
Java

このクラスを見た人は、「LABELS は固定のステータス対応表なんだな」とすぐに理解できます。

Java 8 以前:Collections.unmodifiableMap でラップする

Java 8 以前には Map.of がないので、
Collections.unmodifiableMap を使って不変Mapを作ります。

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

public class ImmutableMapLegacy {

    public static final Map<String, String> STATUS_LABELS;

    static {
        Map<String, String> tmp = new HashMap<>();
        tmp.put("NEW", "新規");
        tmp.put("IN_PROGRESS", "処理中");
        tmp.put("DONE", "完了");
        STATUS_LABELS = Collections.unmodifiableMap(tmp);
    }

    public static void main(String[] args) {
        System.out.println(STATUS_LABELS.get("NEW")); // 新規
        STATUS_LABELS.put("CANCEL", "キャンセル");   // UnsupportedOperationException
    }
}
Java

ここで深掘りしたいポイントは、「ラップ元のMapを外に出さない」ことです。

unmodifiableMap は「ラップしているMapへの変更を禁止するビュー」を返しているだけなので、
元の tmp への参照をどこかに持っていてそこから put されると、中身は変わってしまいます。

だからこそ、

ラップ元のMapはその場で作って、そのまま unmodifiableMap に渡す
ラップ元のMapへの参照を他に持たない

という書き方がとても重要になります。


不変Mapを返すユーティリティを用意する

「呼び出し側に“変えられない”ことを伝える窓口」

業務メソッドの戻り値として、「不変Mapを返したい」こともよくあります。
そのたびに Collections.unmodifiableMapMap.copyOf と書くのは少し重いので、
ユーティリティにまとめてしまうとスッキリします。

Java 9 以降なら、Map.copyOf が使えます。

import java.util.Map;

public final class ImmutableMaps {

    private ImmutableMaps() {}

    public static <K, V> Map<K, V> copyOf(Map<K, V> source) {
        if (source == null || source.isEmpty()) {
            return Map.of();
        }
        return Map.copyOf(source); // 不変Mapを返す
    }
}
Java

使い方はこうです。

Map<String, String> mutable = new java.util.HashMap<>();
mutable.put("A", "あ");
mutable.put("B", "い");

Map<String, String> immutable = ImmutableMaps.copyOf(mutable);

// immutable.put("C", "う"); // UnsupportedOperationException
Java

ここでの重要ポイントは、「元のMapとは独立した“不変のコピー”を返している」ことです。
呼び出し側が元のMapをあとで変更しても、
不変Map側は影響を受けません。

Java 8 以前なら、Collections.unmodifiableMapnew HashMap<>(source) を組み合わせます。

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

public static <K, V> Map<K, V> copyOfLegacy(Map<K, V> source) {
    if (source == null || source.isEmpty()) {
        return Collections.emptyMap();
    }
    return Collections.unmodifiableMap(new HashMap<>(source));
}
Java

「不変Mapにしておくと安心な場所」を具体的にイメージする

例1:コード → ラベルのマスタ

画面や帳票で使う「コード → 表示名」の対応表は、
不変Mapにしておくととても安心です。

public final class PaymentMethods {

    private PaymentMethods() {}

    public static final Map<String, String> LABELS =
            Map.of(
                    "CASH", "現金",
                    "CARD", "クレジットカード",
                    "BANK", "銀行振込"
            );
}
Java

どこかのコードが LABELS.put("POINT", "ポイント") しようとすると例外になり、
「いつの間にか支払い方法が増えていた」という事故を防げます。

例2:設定値を読み込んだ後の「確定した設定マップ」

設定ファイルやDBから一度だけ読み込んで、
あとは読み取り専用で使うような設定マップも、不変Mapに向いています。

public final class AppConfig {

    private final Map<String, String> settings;

    public AppConfig(Map<String, String> settings) {
        this.settings = Map.copyOf(settings); // 不変コピー
    }

    public String get(String key) {
        return settings.get(key);
    }

    public Map<String, String> asMap() {
        return settings; // すでに不変なのでそのまま返してよい
    }
}
Java

ここでの重要ポイントは、「コンストラクタで不変コピーを作ってしまう」ことです。
これにより、外から渡されたMapがあとで変更されても、
AppConfig の内部状態は変わりません。


「不変Map」と「読み取り専用にしたいだけのMap」を区別する

内部状態を守るための設計

ときどき、
「本当は中身を変えたくないけど、実装の都合で可変Mapを返している」
というコードがあります。

// あまり良くない例
public Map<String, String> getSettings() {
    return settings; // 内部の可変Mapをそのまま返している
}
Java

これだと、呼び出し側が getSettings().put(...) できてしまい、
内部状態がどこからでも書き換えられる危険な設計になります。

「外からは変えてほしくない」と思っているなら、
不変Mapを返すべきです。

public Map<String, String> getSettings() {
    return Map.copyOf(settings); // 不変コピーを返す
}
Java

ここでの重要ポイントは、「不変Mapは“意図を伝える道具”でもある」ということです。
「このマップは変えちゃダメ」というメッセージを、
型と実装で表現しているわけです。


まとめ:不変Map生成で身につけてほしい感覚

不変Map生成は、単に「変更できないMapを作るテクニック」ではなく、
「ここは絶対に変わってほしくない」という意図をコードに刻むための技術です。

変わらない対応表・マスタ・設定は Map.of(...)Map.copyOf(...) で不変にする。
Java 8 以前なら Collections.unmodifiableMap(new HashMap<>(source)) で不変ビューを作り、元Mapは外に出さない。
メソッドの戻り値で「外から変更されたくない」ものは、不変Mapを返す。
コンストラクタで不変コピーを作っておくと、外部からの変更の影響を受けない。

もしあなたのコードのどこかに、

public static final Map<String, String> LABELS = new HashMap<>();
Java

のような「定数なのに可変Map」があったら、
それを一度 Map.of(...)Map.copyOf(...) に置き換えてみてください。

その小さな変更が、
「コレクションの“変わる/変わらない”を意識して設計できるエンジニア」への、
確かな一歩になります。

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