MEDIA

メディア

  1. TOP
  2. メディア
  3. プログラミング
  4. Java Mapの初期化 完全ガイド|基本・Stream活用・Map.ofまで

Java Mapの初期化 完全ガイド|基本・Stream活用・Map.ofまで

Javaプログラミングにおいて、キーと値をペアで管理する Map インターフェースの初期化は、最も頻繁に行われる操作の一つです。しかし、その方法は多岐にわたり、要件に応じた最適な選択が求められます。

そこで本記事では、Java Mapの初期化方法を要件別に整理し、最適な選択ができるようガイドします。具体的には、伝統的な HashMap の使い方から、Java 8 の Stream API(Collectors.toMap)、そして Java 9 以降の Map.of まで、実用的なコード例と共に網羅的に解説します。

したがって、この記事を読み終える頃には、あなたのプロジェクト要件に最も適した、安全でパフォーマンスの良いMap初期化コードを迷わず選択できるようになります。

要件別:Java Map初期化の最適解マップ

まず、Mapの初期化方法を選ぶ上で最も重要な軸は、後から要素を変更・追加するかどうかです。つまり、可変(Mutable)Mapか不変(Immutable)Mapかを選択します。

一方で、順序やソートの要件、初期要素数、コレクションからの変換など、他の要因も選択に影響します。まずは、以下の指針を参考にしてください。

可変Mapが必要なケースでは、プログラム実行中に要素を追加・削除する必要がある場合に選択します。選択肢としては、HashMap(順序不要・高速)、LinkedHashMap(挿入順・アクセス順)、TreeMap(キーでソート)があります。

一方、不変Mapが必要なケースでは、定数や設定値として一度定義したら変更しない場合に適しています。選択肢としては、Map.ofMap.ofEntries(Java 9以降)、Collections.singletonMap(要素が1つだけ)が挙げられます。

さらに、コレクションからの変換が必要な場合、既存の List や配列からMapを生成したいときは、Stream API と Collectors.toMap(Java 8以降)を使用します。

近年の開発現場では、LTS(長期サポート)バージョンである Java 17 や Java 21 への移行が加速しています(JetBrains The State of Developer Ecosystem 2024 等を参照)。これにより、本記事で解説する Java 8 の Stream API や Java 9 の Map.of といったモダンな機能は、もはや新しい書き方ではなく、現場の標準的な書き方として定着しています。

基本:putメソッドによる可変Mapの初期化

まず、最も伝統的かつ基本的な方法は、空のMapインスタンスを生成し、put メソッドで要素を一つずつ追加していく方法です。プログラムのロジックの途中で動的にMapを構築する場合に適しています。

HashMap:順序不要で最速な基本形

特別な要件(順序保持やソート)がない限り、HashMap を使うのが一般的です。これは、キーのハッシュ値に基づいて値を格納するため、高速な検索(O(1) 定数時間)が可能です。

例えば、以下のように初期化します。

Map<String, Integer> userAgeMap = new HashMap<>();

userAgeMap.put("Alice", 20);
userAgeMap.put("Bob", 30);
userAgeMap.put("Carol", 25);

if (userAgeMap.containsKey("Alice")) {
    System.out.println("Alice's age: " + userAgeMap.get("Alice"));
}

この方法は、Mapの生成と要素の追加が明確に分離されるため、可読性が高いのが特徴です。

初期容量の指定とパフォーマンス

HashMap には初期容量(Initial Capacity)と負荷係数(Load Factor)という概念があります。デフォルトでは容量16、負荷係数0.75に設定されています。

これは、要素数が 16 * 0.75 = 12 を超えると、内部配列のサイズが自動的に拡張(リハッシュ)されることを意味します。

したがって、あらかじめ大量の要素(例:1000件)を入れることが分かっている場合は、初期容量を指定してインスタンス化するのがベストプラクティスです。これにより、無駄なリハッシュ処理のコストを削減できます。

int expectedSize = 1000;
int initialCapacity = (int) (expectedSize * 4 / 3) + 1;

Map<String, String> largeMap = new HashMap<>(initialCapacity);

for (int i = 0; i < expectedSize; i++) {
    largeMap.put("key" + i, "value" + i);
}

LinkedHashMap:挿入順またはアクセス順の保持

もし、Mapに要素を追加した挿入順序を保持したい場合は、LinkedHashMap を使用します。

Map<String, Integer> insertionOrderMap = new LinkedHashMap<>();

insertionOrderMap.put("One", 1);
insertionOrderMap.put("Three", 3);
insertionOrderMap.put("Two", 2);

insertionOrderMap.forEach((k, v) -> System.out.println(k + ": " + v));

さらに、LinkedHashMap はコンストラクタの引数に accessOrder = true を渡すことで、アクセス順(最後にアクセスされた要素が末尾に来る)に要素を保持できます。これは、LRU(Least Recently Used)キャッシュを実装する際に非常に便利です。

Map<String, String> lruCache = new LinkedHashMap<>(3, 0.75f, true);

lruCache.put("A", "Apple");
lruCache.put("B", "Banana");
lruCache.put("C", "Cherry");

lruCache.get("A");

TreeMap:キーによる自動ソート

一方で、Mapのキーを常にソートされた状態(自然順序または指定した順序)で保持したい場合は、TreeMap を使用します。これは赤黒木(Red-Black Tree)に基づいた実装です。

Map<String, Integer> sortedMap = new TreeMap<>();

sortedMap.put("Bob", 30);
sortedMap.put("Carol", 25);
sortedMap.put("Alice", 20);

sortedMap.forEach((k, v) -> System.out.println(k + ": " + v));

避けるべき初期化方法:アンチパターン

Mapを初期化する方法として、かつて使われていたものの現在では非推奨とされるテクニックが存在します。

ダブルブレース初期化の問題点

以下のような構文を見たことがあるかもしれません。ダブルブレース初期化または二重中括弧初期化と呼ばれるイディオムです。

Map<String, String> antiPatternMap = new HashMap<>() {{
    put("key1", "value1");
    put("key2", "value2");
}};

この方法は、一見すると簡潔に見えます。しかし、内部的には HashMap を継承した無名内部クラスを生成し、そのインスタンスイニシャライザで put を呼び出しています。

この手法には以下のような重大な欠点があるため、使用は避けるべきです。まず、パフォーマンスとメモリの観点から、余計な無名クラスが生成されるため、クラスローディングのオーバーヘッドやメモリ消費が発生します。さらに、メモリリークの危険性があり、特に内部クラスの特性上、意図しないメモリリークを引き起こす可能性があります。加えて、可読性の低下も問題であり、Java 9 以降の Map.of と比較して構文が冗長で、初学者を混乱させます。

したがって、Java 9以降が使える環境では Map.of を、Java 8以前でも Stream API や地道な put を使うべきです。

実践(Java 8):Stream API と Collectors.toMap

Java 8 で Stream API が導入されたことで、既存のコレクション(ListSet、配列など)からMapを効率的に生成できるようになりました。java.util.stream.Collectors クラスの toMap メソッドを使用します。

ListやArrayからMapを生成する

例えば、User オブジェクトのリストがあり、それをユーザーIDをキー、Userオブジェクト自身を値とするMapに変換したい場合、以下のように書けます。

class User {
    private final String id;
    private final String name;
    
    public String getId() { return id; }
    public String getName() { return name; }
}

List<User> userList = List.of(
    new User("u001", "Alice"),
    new User("u002", "Bob"),
    new User("u003", "Carol")
);

Map<String, User> userMap = userList.stream()
    .collect(Collectors.toMap(
        User::getId,
        user -> user
    ));

System.out.println(userMap);

Collectors.toMap は、第1引数でキーを、第2引数で値をどのように抽出するかを指定します。詳細は Oracle公式ドキュメント(Collectors) でも確認できます。

注意点:キー重複とIllegalStateExceptionへの対処

Collectors.toMap を使う上で最も重要な注意点は、キーの重複です。もしストリーム内に同じキーが複数存在した場合、toMap はデフォルトで IllegalStateException をスローします。

List<String> items = List.of("Apple", "Banana", "Apricot");

try {
    Map<Character, String> map = items.stream()
        .collect(Collectors.toMap(
            s -> s.charAt(0),
            s -> s
        ));
} catch (IllegalStateException e) {
    System.err.println(e.getMessage());
}

この問題を解決するには、第3引数としてマージ関数(Merge Function)を指定します。マージ関数は、キーが重複した場合に古い値(oldValue)と新しい値(newValue)をどのように扱うかを定義します。

List<String> items = List.of("Apple", "Banana", "Apricot");

Map<Character, String> map = items.stream()
    .collect(Collectors.toMap(
        s -> s.charAt(0),
        s -> s,
        (oldValue, newValue) -> newValue
    ));

System.out.println(map);

順序保持(LinkedHashMap)を指定する

デフォルトでは、Collectors.toMap が返すMapの実装(HashMap)は順序を保証しません。もし入力リストの順序を保持したい場合は、第4引数(mapSupplier)で LinkedHashMap::new を指定します。

List<User> userList = List.of(
    new User("u003", "Carol"),
    new User("u001", "Alice"),
    new User("u002", "Bob")
);

Map<String, User> orderedUserMap = userList.stream()
    .collect(Collectors.toMap(
        User::getId,
        user -> user,
        (v1, v2) -> v1,
        LinkedHashMap::new
    ));

System.out.println(orderedUserMap.keySet());

実践(Java 9以降):Map.of / Map.ofEntriesによる不変Map

Java 9 で、不変(Immutable)Mapを簡単に作成するためのファクトリメソッド Map.of シリーズが導入されました。これは、主に定数や設定値など、一度定義したら変更しないMapを定義するのに最適です。

Map.of:少数の定数マップを簡潔に定義

Map.of は、0個から最大10個のキー/値ペアまで、引数の数に応じたオーバーロードが用意されています。これにより、非常に簡潔に不変Mapを定義できます。

Map<String, String> emptyMap = Map.of();

Map<String, String> map1 = Map.of("key1", "value1");

Map<Integer, String> statusMap = Map.of(
    200, "OK",
    404, "Not Found",
    500, "Internal Server Error"
);

Map<String, Integer> map10 = Map.of(
    "k1", 1, "k2", 2, "k3", 3, "k4", 4, "k5", 5,
    "k6", 6, "k7", 7, "k8", 8, "k9", 9, "k10", 10
);

Map.of で作成されたMapは完全不変です。もし putremove を呼び出そうとすると、UnsupportedOperationException がスローされます。

try {
    statusMap.put(403, "Forbidden");
} catch (UnsupportedOperationException e) {
    System.err.println("このMapは変更できません。");
}

Map.ofEntries:11件以上の定数マップ

もし、11件以上の要素を持つ不変Mapを作成したい場合、または Map.of の引数にペアを羅列するのが見づらい場合は、Map.ofEntries を使います。このメソッドは、Map.Entry の可変長引数を取ります。

Map.Entry は、Map.entry(key, value) という便利なstaticメソッドで作成できます。

import static java.util.Map.entry;

Map<String, String> largeImmutableMap = Map.ofEntries(
    entry("key1", "value1"),
    entry("key2", "value2"),
    entry("key3", "value3"),
    entry("key11", "value11"),
    entry("key12", "value12")
);

Map.of の利点と注意点(null不可・重複不可)

Map.of シリーズには、不変性以外にも重要な特性(制約)があります。まず、nullを許容しません。キーも値も null にすることはできず、もし null を渡そうとすると実行時に NullPointerException がスローされます。次に、キーの重複を許容しません。同じキーを複数指定すると実行時に IllegalArgumentException がスローされます。さらに、高効率であり、内部的に最適化されており従来の HashMap よりもメモリ効率が高く生成も高速です。

不変Mapの使い分け:Map.of vs Collections.unmodifiableMap

Java 9 より前は、不変Mapを作るために Collections.unmodifiableMap というメソッドが使われていました。しかし、これと Map.of は似て非なるものであり、注意が必要です。

Collections.unmodifiableMap の仕組みと注意点

Collections.unmodifiableMap は、引数として渡された既存の可変Mapをラップ(包み込む)し、変更メソッド(putremove)が呼ばれたら例外をスローするビュー(View)を返します。

問題は、元の可変Mapへの参照が残っていると、その元Mapを変更できてしまう点です。そして、元Mapが変更されると、ラップした不変ビューからもその変更が見えてしまいます。

Map<String, String> mutableMap = new HashMap<>();
mutableMap.put("key1", "value1");

Map<String, String> unmodifiableView = Collections.unmodifiableMap(mutableMap);

mutableMap.put("key2", "value2");

System.out.println(unmodifiableView);

このように、Collections.unmodifiableMap は真の不変性を保証しません。一方で、Map.of は生成された時点から誰にも変更できない、真に不変なMapを返します。

したがって、定数としてMapを定義する場合は、Java 9以降であれば Map.of を使うのが常に堅実です。

防御的コピー(Defensive Copy)の重要性

もし、Java 8 以前の環境や、動的に構築したMapを不変にして外部(他のメソッドやクラス)に渡したい場合は、防御的コピー(Defensive Copy)と unmodifiableMap を組み合わせるテクニックが使われます。

class ConfigService {
    private final Map<String, String> configs;

    public ConfigService() {
        Map<String, String> internalMap = new HashMap<>();
        internalMap.put("db.url", "jdbc:...");
        internalMap.put("db.user", "admin");
        
        this.configs = Collections.unmodifiableMap(new HashMap<>(internalMap));
    }

    public Map<String, String> getConfigs() {
        return this.configs;
    }
}

まとめ:用途別・Javaバージョン別 初期化早見表

最後に、これまで見てきたJava Mapの初期化方法を要件別に整理します。

まず、変更する・順序不要の場合は HashMap を選択します。最も一般的で高速な可変Mapです。要素数が予測できる場合は new HashMap<>(初期容量) でパフォーマンスを最適化します。

次に、変更する・順序保持が必要な場合は LinkedHashMap を使います。要素を put した順序で保持したい場合に適しており、accessOrder=true を指定すればLRUキャッシュにも応用可能です。

さらに、変更する・キーソートが必要なら TreeMap を選びます。キーを常に自然順序(または指定したComparator順)でソート状態に保ちます。

一方で、変更しない・定数として使う場合(Java 9以降)は Map.of または Map.ofEntries が最も推奨される不変Mapの作成方法です。null やキー重複は許容されず、10件以下なら Map.of、それ以上なら Map.ofEntries が便利です。

また、変更しない・1件のみの場合(Java 8)は Collections.singletonMap を使えます。Java 9 の Map.of(k, v) が使えるならそちらが推奨されますが、Java 8環境で1件だけの不変Mapが必要な場合に手軽です。

最後に、コレクションから生成する場合(Java 8以降)は StreamCollectors.toMap を使用します。List や配列からMapへ変換する際の標準的な方法であり、キー重複時のマージ戦略(第3引数)と順序保持(第4引数)を明示的に指定することで柔軟なMap生成が可能です。

したがって、Mapを初期化する際は、まず変更するか・しないかを決定し、次に順序やソートが必要かを考慮することで、最適な実装が自動的に決まります。本記事で紹介した各手法を適切に使い分けることで、安全でパフォーマンスの高いJavaコードを実現できます。

もしJavaの構文や HashMap の内部動作で詰まったら、現役エンジニアが伴走する Zerocode Online で実務レベルのJavaを体系的に学ぶのも有効です。

ドライブラインは「未経験からエンジニア」を本気で応援します

ドライブラインでは、社会人を含む未経験者がエンジニアへキャリアチェンジできる実践的な育成カリキュラムを提供しています。設計・開発・テスト・運用まで、現場で通用するスキルを体系的に学べます。

実務直結カリキュラム

要件定義〜Git運用・レビューまで、実践形式で習得可能。

メンター伴走型サポート

現役エンジニアが1on1で学習とポートフォリオを支援。

SE志向の育成

PG止まりから脱却し、上流工程を担える人材へ。

転職・案件参画サポート

OJT・案件参画・キャリア設計まで一貫支援。

「未経験から本気でエンジニアを目指したい」あなたへ。まずはドライブラインの採用ページで詳細をご覧ください。

 

Join us! 未経験からエンジニアに挑戦できる環境で自分の可能性を信じてみよう 採用ページを見る→

記事監修

ドライブライン編集部

[ この記事をシェアする ]

記事一覧へ戻る