MEDIA

メディア

  1. TOP
  2. メディア
  3. プログラミング
  4. JavaのMap徹底解説:基本から応用まで

JavaのMap徹底解説:基本から応用まで

本記事では、JavaのMapインターフェースについて解説します。Mapは、キーと値のペアを効率的に扱うための仕組みです。例えば、プログラミングの現場では多種多様なケースで使われる、重要なコレクションの一つと言えます。

まず、Mapを正しく理解し活用することで、開発効率は大きく向上します。具体的には、データの検索や格納、ソート、並列処理などが容易になります。そのため、この記事では初心者の方にもわかりやすいよう、基本から一歩ずつ進めます。

さらに、各セクションではMapの概要や代表的な実装クラスを解説します。便利なメソッドの使い方だけでなく、パフォーマンスやスレッドセーフの観点にも触れます。したがって、ぜひこれを機にJavaのMapに関する知識を深めてください。


JavaのMapとは?基本特徴とメリット

JavaにおけるMapとは、「キー(Key)」と「値(Value)」を一対にして格納するデータ構造です。このインターフェースはjava.util.Mapとして提供されています。

Mapの最大の特徴は、キーを使って値を素早く検索できる点にあります。例えば、辞書で単語(キー)を引いて意味(値)を調べる操作と似ています。一方で、ListやSetとは異なり、同じキーを重複して格納できない制約があります。これにより、特定のキーに紐づく値を一意に管理するのに最適です。

Mapを利用する代表的な場面には、様々なものがあります。例えば、ユーザーID(キー)とユーザー情報(値)を紐づけるケースです。あるいは、商品ID(キー)に商品データ(値)を関連付ける場合にも使われます。

このように、Mapは散在する情報を整理し、関連付けて管理する際に非常に便利です。さらに、大量の情報を保持している場合でも、キーを使った検索が高速に行えます。このため、データ取得の効率化に大いに役立ちます。

また、実装クラスによっては順序保持やソートなどの付加機能も提供されます。したがって、本記事で後述するHashMapやTreeMapなど、要件に合わせて使い分けることが肝要です。より詳細な仕様は、Oracleの公式ドキュメントでも確認できます。


Mapの基本操作(宣言・追加・取得・削除)

Mapを扱うための基本的な操作方法を解説します。宣言から初期化、要素の追加、取得、削除までを見ていきましょう。

宣言と初期化

まず、Mapを利用する際はジェネリクスで型を指定して宣言します。Map<KeyType, ValueType>という形式です。例えば、キーが文字列型、値が整数型のMapは以下のように宣言します。

初期化時には、HashMapやTreeMapなど、用途に応じた実装クラスを選ぶ必要があります。

// キーがString型、値がInteger型のMapを宣言
// 実装クラスとしてHashMapを選択
Map<String, Integer> userScores = new HashMap<>();

// キーがInteger型、値がString型のMapを宣言
// 実装クラスとしてTreeMapを選択 (キーでソートされる)
Map<Integer, String> products = new TreeMap<>();

// 実装クラスとしてLinkedHashMapを選択 (挿入順が保持される)
Map<String, String> settings = new LinkedHashMap<>();

どの実装クラスを選ぶかは、検索性能や順序保持、ソート要件などを考慮して決めます。

【Java 9〜】不変Mapの簡単な作成方法

Java 9以降、不変(Immutable)なMapを簡単に作成するMap.of()メソッドが導入されました。不変Mapとは、作成後に要素の追加や削除ができないMapのことです。

この方法は、テストデータやアプリケーションの固定的な設定値を定義する際に非常に便利です。現在の最新LTS環境下でも、この方法は広く使われています。

// Java 9以降で利用可能な不変Mapの作成
Map<String, String> capitals = Map.of(
    "Japan", "Tokyo",
    "USA", "Washington, D.C.",
    "France", "Paris"
);

// このMapに要素を追加しようとすると例外が発生する
// capitals.put("Germany", "Berlin"); // UnsupportedOperationException

// Java 10では既存のMapから不変なコピーを作成する Map.copyOf() も追加された
Map<String, Integer> mutableMap = new HashMap<>();
mutableMap.put("A", 1);
Map<String, Integer> immutableCopy = Map.copyOf(mutableMap);

値の追加と取得 (put, get)

Mapに要素(キーと値のペア)を追加するにはputメソッドを使います。既に追加したいキーが存在する場合、値は新しいもので上書きされます。

一方、要素を取得するにはgetメソッドを使います。指定したキーが存在しない場合、getメソッドはnullを返します。

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

// put: 値の追加
scores.put("Alice", 100);
scores.put("Bob", 85);
scores.put("Alice", 95); // キー "Alice" の値が 100 から 95 に上書きされる

// get: 値の取得
Integer aliceScore = scores.get("Alice"); // 95 が返る
Integer charlieScore = scores.get("Charlie"); // キーが存在しないため null が返る

System.out.println(aliceScore); // 95
System.out.println(charlieScore); // null

値の削除 (remove, clear)

Mapから特定の要素を削除するにはremoveメソッドを使用します。remove(キー)と指定すると、該当するキーと値のペアが削除されます。

また、Map内のすべての要素を一度に削除したい場合はclearメソッドを使います。

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 85);

// remove: 特定のキーの要素を削除
scores.remove("Bob"); // "Bob" のエントリが削除される
System.out.println(scores.get("Bob")); // null

// clear: 全ての要素を削除
scores.clear();
System.out.println(scores.size()); // 0

存在チェック (containsKey, containsValue)

Map内に特定のキーや値が存在するかを調べるメソッドも用意されています。containsKeyは指定したキーの存在を、containsValueは指定した値の存在をチェックします。

ここで重要な注意点があります。containsKeyはキーを元に高速に検索(O(1)またはO(log n))します。しかし、containsValueはMap内の全ての値を順番に調べる(O(n))ため、要素数が多いとパフォーマンスが低下する可能性があります。

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);
scores.put("Bob", 85);

// containsKey: キーの存在チェック (高速)
boolean hasAlice = scores.containsKey("Alice"); // true
boolean hasCharlie = scores.containsKey("Charlie"); // false

// containsValue: 値の存在チェック (低速になる可能性あり)
boolean has95 = scores.containsValue(95); // true
boolean has70 = scores.containsValue(70); // false

代表的なMap実装クラスの比較

JavaにはMapインターフェースを実装したクラスがいくつかあります。それぞれ特徴が異なるため、用途に応じて適切に選択することが重要です。ここでは代表的な4つを紹介します。

実装クラス 特徴 順序性 null許容 パフォーマンス スレッドセーフ
HashMap 最も標準的。ハッシュテーブルに基づく。 保証されない キー(1つ), 値 OK 平均 O(1) なし
LinkedHashMap HashMapにリンクリストを追加。 挿入順 or アクセス順 キー(1つ), 値 OK 平均 O(1) なし
TreeMap 木構造(赤黒木)に基づく。 キーの自然順序 or 指定順序 (ソート) キー NG, 値 OK O(log n) なし
ConcurrentHashMap 並列処理(マルチスレッド)向け。 保証されない キー NG, 値 NG HashMapに近い あり

HashMap: 高速だが順序不定

HashMapは、最も一般的に使用されるMap実装です。内部的にハッシュテーブルという仕組みを使い、キーのハッシュ値を計算して格納場所を決めます。

この仕組みにより、要素の追加(put)や取得(get)が平均してO(1)という非常に高速な時間で行えます。ただし、ハッシュ値に基づくため、要素が格納される順序は保証されません。したがって、順序を気にする必要がなく、高速なアクセスが必要な場合に最適です。

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

LinkedHashMapは、HashMapの機能に加え、要素が追加された順序(挿入順)を保持する機能を持っています。内部的にリンクリストでエントリを管理しているためです。

さらに、コンストラクタの設定によっては、要素がアクセスされた順(アクセス順)に並び替えることも可能です。この特性は、LRU(Least Recently Used)キャッシュなどを実装する際に役立ちます。そのため、HashMapと同等のO(1)性能を持ちつつ、順序を保持したい場合に選択します。

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

TreeMapは、キーを自動的にソートして格納するMap実装です。内部的に赤黒木という木構造データを使用しています。

キーの「自然順序」(数値の昇順、文字列の辞書順など)に従ってソートされます。あるいは、Comparatorを指定して独自のソート順を定義することも可能です。要素の追加・取得・削除はO(log n)の時間がかかります。したがって、HashMapよりは低速ですが、常にキーがソートされた状態を保ちたい場合に有用です。

ConcurrentHashMap: スレッドセーフと並列処理

ConcurrentHashMapは、マルチスレッド環境での使用に特化したMap実装です。複数のスレッドから同時にアクセスされても、データが壊れないように(スレッドセーフ)設計されています。

従来のHashtableやCollections.synchronizedMapよりも高い並列処理性能を持ちます。Map全体をロックするのではなく、内部を細かく分割してロックするため、多くのスレッドが同時に読み書きを行えます。ただし、その分HashMapよりも若干のオーバーヘッドがあります。


Mapの便利な標準メソッド (Java 8以降)

Java 8では、Mapの操作をより簡潔かつ安全に行うための便利なメソッドが多数追加されました。これらを活用することで、冗長なif文やnullチェックを削減できます。

getOrDefault: null回避とデフォルト値

getメソッドはキーが存在しないとnullを返しますが、getOrDefaultはキーが存在しない場合に指定したデフォルト値を返します。

これにより、getの結果がnullかどうかを毎回チェックする手間が省けます。特に、値がnullだった場合に例外が発生する(NullPointerException)のを防ぐのに役立ちます。

Map<String, Integer> scores = new HashMap<>();
scores.put("Alice", 95);

// "Alice" は存在するので 95 が返る
int aliceScore = scores.getOrDefault("Alice", 0); // 95

// "Bob" は存在しないのでデフォルト値 0 が返る
int bobScore = scores.getOrDefault("Bob", 0); // 0

// 従来のgetだとnullが返り、int型変数に代入しようとすると例外が発生する可能性がある
// Integer bobScoreRaw = scores.get("Bob"); // null
// int score = bobScoreRaw; // NullPointerException

putIfAbsent, replace: 条件付き更新

putIfAbsentは、指定したキーがMapに存在しない場合(または値がnullの場合)にのみ、値を登録します。すでにキーが存在する場合は何もしません。

一方、replaceは、指定したキーがMapに存在する場合にのみ、値を更新(上書き)します。

Map<String, String> settings = new HashMap<>();
settings.put("theme", "dark");

// "theme" は既に存在するので、"light" には上書きされない
settings.putIfAbsent("theme", "light"); // "dark" のまま

// "font" は存在しないので、"medium" が登録される
settings.putIfAbsent("font", "medium");

System.out.println(settings.get("theme")); // "dark"
System.out.println(settings.get("font")); // "medium"

// "theme" は存在するので "white" に更新される
settings.replace("theme", "white");
System.out.println(settings.get("theme")); // "white"

compute, merge: 値の動的な計算とマージ

computeやmergeは、さらに高度な操作を提供します。これらは、既存の値に基づいて新しい値を計算し、Mapに反映させます。

例えば、単語の出現回数をカウントするような集計処理を非常に簡潔に記述できます。

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

// "merge" を使った単語カウント
// キーが存在しない場合は 1 を設定
// 存在する場合は、既存の値(v1)と新しい値(v2=1)を合計(v1 + v2)する
wordCounts.merge("apple", 1, (v1, v2) -> v1 + v2);
wordCounts.merge("banana", 1, (v1, v2) -> v1 + v2);
wordCounts.merge("apple", 1, (v1, v2) -> v1 + v2);

System.out.println(wordCounts.get("apple")); // 2
System.out.println(wordCounts.get("banana")); // 1

// "compute" を使った例 (キーが存在する場合のみ値を更新)
wordCounts.computeIfPresent("apple", (key, oldValue) -> oldValue * 10);
System.out.println(wordCounts.get("apple")); // 20

Mapのループ処理と反復

Mapに格納された全ての要素を順に処理(反復)する方法はいくつかあります。それぞれの特徴を理解して使い分けることが重要です。

キーの集合 (keySet) でループ

最も古くからある方法の一つがkeySet()を使う方法です。これはMap内のすべてのキーをSetとして取得します。その後、get()メソッドを使ってキーに対応する値を取得します。

ただし、この方法はループのたびにget()を呼び出すため、entrySet()を使う方法(後述)に比べてパフォーマンスが劣る場合があります。

Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 85);

for (String key : scores.keySet()) {
    Integer value = scores.get(key);
    System.out.println("Key: " + key + ", Value: " + value);
}

値のコレクション (values) でループ

キーは不要で、値だけを処理したい場合はvalues()を使います。これはMap内のすべての値をCollectionとして取得します。

Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 85);
int total = 0;

for (Integer value : scores.values()) {
    total += value;
}
System.out.println("Total score: " + total); // 180

エントリ (entrySet) でループ (推奨)

キーと値の両方を同時に扱いたい場合、このentrySet()を使う方法が最も効率的で推奨されます。entrySet()は、キーと値のペア(Map.Entryオブジェクト)のSetを返します。

この方法なら、ループ内でget()を呼び出す必要がなく、キーと値の両方を一度に取り出せます。

Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 85);

for (Map.Entry<String, Integer> entry : scores.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    System.out.println("Key: " + key + ", Value: " + value);
}

forEachメソッド (Java 8) での反復

Java 8以降では、ラムダ式を使ってより簡潔にループ処理を記述できるforEachメソッドが利用できます。forEachはキーと値を引数に取るため、entrySetと同様に効率的です。

Map<String, Integer> scores = Map.of("Alice", 95, "Bob", 85);

scores.forEach((key, value) -> {
    System.out.println("Key: " + key + ", Value: " + value);
});

Mapのソートとフィルタリング (Stream API)

HashMapのような順序を保証しないMapでも、Java 8で導入されたStream APIを使えば、簡単にソートやフィルタリング(絞り込み)を行えます。

キーや値によるソート (Stream.sorted)

MapのentrySet()からStreamを生成し、sorted()メソッドを使って並び替えます。キーでソートしたり、値でソートしたりすることが可能です。

ソート結果は、LinkedHashMapに集める(collectする)ことで、ソートされた順序を保持したMapとして得られます。

Map<String, Integer> scores = Map.of("Bob", 85, "Alice", 95, "Charlie", 70);

// 1. キー (名前) でソート (昇順)
Map<String, Integer> sortedByKey = scores.entrySet().stream()
    .sorted(Map.Entry.comparingByKey())
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (e1, e2) -> e1, // 衝突時のマージ戦略 (ここでは不要だが必須)
        LinkedHashMap::new // 順序を保持するMapに格納
    ));
// sortedByKey: {Alice=95, Bob=85, Charlie=70}

// 2. 値 (スコア) でソート (降順)
Map<String, Integer> sortedByValueDesc = scores.entrySet().stream()
    .sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue,
        (e1, e2) -> e1,
        LinkedHashMap::new
    ));
// sortedByValueDesc: {Alice=95, Bob=85, Charlie=70}

条件によるフィルタリング (Stream.filter)

Streamのfilter()メソッドを使えば、特定の条件を満たすエントリだけを抽出できます。例えば、「スコアが80点以上」といった条件で絞り込めます。

Map<String, Integer> scores = Map.of("Bob", 85, "Alice", 95, "Charlie", 70);

// 値 (スコア) が 80 以上のエントリのみを抽出
Map<String, Integer> highScores = scores.entrySet().stream()
    .filter(entry -> entry.getValue() >= 80)
    .collect(Collectors.toMap(
        Map.Entry::getKey,
        Map.Entry::getValue
    ));
// highScores: {Alice=95, Bob=85}

Map利用時の重要知識と注意点

Mapは非常に便利ですが、安全かつ効率的に使うためには、いくつかの重要な注意点があります。特にnullの扱いや、自作オブジェクトをキーにする場合のequalsとhashCodeです。

nullキー・null値の許容範囲 (実装クラス別)

すべてのMap実装がnullを許可するわけではありません。これを誤解していると、予期せぬNullPointerExceptionに見舞われます。

  • HashMap, LinkedHashMap: nullキーを1つだけ許可します。null値はいくつでも許可します。
  • TreeMap: nullキーを許可しません(キーの比較ができないためNullPointerExceptionが発生します)。null値は許可します(Comparatorがnullを扱える場合を除く)。
  • ConcurrentHashMap: パフォーマンスとスレッドセーフの観点から、nullキーもnull値も許可しません。

特に、get(key)がnullを返した場合、それは「キーが存在しない」のか「キーは存在するが、値としてnullが格納されている」のか区別がつきません。このため、nullを値として扱う場合はcontainsKeyを併用するなど、慎重な設計が求められます。

equalsとhashCodeの規約 (カスタムオブジェクトをキーにする場合)

Map、特にHashMapやLinkedHashMapを正しく動作させる上で、最も重要かつ見落とされがちなのがこの規約です。

HashMapは、キーのhashCode()メソッドが返す値(ハッシュ値)を使って格納場所を計算します。そして、同じハッシュ値の場所に複数のキーがある場合(ハッシュ衝突)、equals()メソッドを使ってキーが同一かどうかを判定します。

もし自作クラス(例: Userクラス)をキーとして使う場合、以下の2つの規約(契約)を必ず守る必要があります。

  1. equals()でtrueとなる2つのオブジェクトは、必ず同じhashCode()の値を返さなければならない。
  2. equals()でfalseとなる場合でも、hashCode()が同じ値を返してもよい(ただし、パフォーマンスは低下する)。

この規約を破ると、putしたはずのキーでgetしてもnullが返る、といった不可解な動作を引き起こします。したがって、自作クラスをキーにする場合は、必ずequals()とhashCode()を両方とも正しくオーバーライドしてください。

import java.util.Objects;

class User {
    private final int id;
    private final String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // IDE(Eclipse, IntelliJ IDEAなど)で自動生成するのが安全
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return id == user.id; // IDが同じなら同一ユーザーとみなす
    }

    @Override
    public int hashCode() {
        return Objects.hash(id); // equalsで使うフィールドからハッシュ値を生成
    }
}

// ...
Map<User, String> userRoles = new HashMap<>();
User user1 = new User(1, "Alice");
userRoles.put(user1, "Admin");

// equalsとhashCodeが正しく実装されていれば、
// 別のインスタンスだが「同一」とみなされるキーで値を取得できる
User user2 = new User(1, "Alice_new_name"); // IDが 1
System.out.println(userRoles.get(user2)); // "Admin" が返る

まとめ:JavaのMapを活用しよう

本記事では、JavaのMapインターフェースについて、基本から応用まで幅広く解説しました。Mapは、キーと値のペアでデータを効率的に管理するための強力なコレクションです。

最後に、Mapを効果的に使いこなすためのポイントを振り返ります。

  1. 適切な実装クラスの選択:
    • 速度優先、順序不要ならHashMap。
    • 挿入順やアクセス順が必要ならLinkedHashMap。
    • キーでソートが必要ならTreeMap。
    • マルチスレッド環境ならConcurrentHashMap。
  2. Java 8/9以降の活用:
    • getOrDefaultでnullチェックを減らす。
    • Map.of()で安全な不変Mapを作成する。
    • computeやmergeで複雑な集計を簡潔に書く。
    • Stream APIでソートやフィルタリングを行う。
  3. 重要な規約の遵守:
    • nullの許容範囲を実装クラスごとに把握する。
    • カスタムオブジェクトをキーにする際はequalsとhashCodeを必ず正しく実装する。

これらの特性を理解し、要件に応じて最適なMapを選択・活用することで、プログラムの柔軟性、保守性、そしてパフォーマンスを大きく向上させることが可能です。さらに詳しい情報は、Oracle公式のJavaチュートリアルなども参照してください。

もしJavaの構文や例外処理で詰まったら、現役エンジニアが伴走するZerocode Onlineで実務レベルのJavaを体系的に学ぶのも有効です。実践形式の学習環境で、現場で通用するスキルを着実に身につけられます。

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

記事監修

ドライブライン編集部

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

記事一覧へ戻る