Map インターフェース

キーと値のペアを格納するコレクションとその実装クラスの特性と効率的な使い方

Map インターフェースの概要

Map<K, V> インターフェースは、キーと値のペアを格納するコレクションを定義します。各キーは一意であり、それぞれのキーは最大で1つの値にマッピングされます。実世界の辞書や電話帳のように、キーを使って関連する値を素早く検索できます。

Map の主な特徴

  • キーの一意性: 各キーは Map 内で一意でなければなりません
  • キーと値のマッピング: 各キーは1つの値にマッピングされます
  • キー検索の効率性: キーを使った値の検索が効率的に行えます
  • Null許容: 実装によっては null キーや null 値を許容します

主な Map 実装クラス

HashMap

HashMap はハッシュテーブルに基づく Map インターフェースの実装です。内部的にはキーのハッシュ値を使用して値を格納・検索します。

HashMap の特徴

  • 高速なアクセス: put(k,v)、get(k)、containsKey(k) 操作は平均 O(1) の時間計算量
  • 順序なし: 要素の順序は保証されません
  • null許容: null キーと null 値の両方を許容します
  • 非同期: スレッドセーフではないため、複数スレッドでの使用には外部同期が必要
  • 容量と負荷係数: 初期容量と負荷係数を調整してパフォーマンスを最適化可能

HashMap の基本的な使用例

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

public class HashMapExample {
    public static void main(String[] args) {
        // HashMap の作成
        Map<String, Integer> ages = new HashMap<>();
        
        // キーと値のペアを追加
        ages.put("田中", 25);
        ages.put("佐藤", 30);
        ages.put("鈴木", 28);
        
        // 値の取得
        int tanaka_age = ages.get("田中");  // 25
        
        // キーの存在確認
        boolean hasKey = ages.containsKey("山田");  // false
        
        // 値の存在確認
        boolean hasValue = ages.containsValue(30);  // true
        
        // キーが存在しない場合のデフォルト値
        int yamada_age = ages.getOrDefault("山田", 0);  // 0
        
        // 値の更新
        ages.put("田中", 26);  // 既存の値を上書き
        
        // 条件付き操作
        ages.putIfAbsent("山田", 22);  // キーが存在しない場合のみ追加
        
        // エントリの削除
        ages.remove("鈴木");  // キー"鈴木"とその値を削除
        
        // マップのサイズ
        int size = ages.size();  // 3
        
        // マップの走査
        for (Map.Entry<String, Integer> entry : ages.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // キーセットの取得
        for (String name : ages.keySet()) {
            System.out.println(name);
        }
        
        // 値コレクションの取得
        for (Integer age : ages.values()) {
            System.out.println(age);
        }
    }
}

LinkedHashMap

LinkedHashMap は HashMap を拡張し、挿入順序または最近アクセス順序を保持する機能を追加した実装です。

LinkedHashMap の特徴

  • 予測可能な反復順序: デフォルトでは挿入順序を保持
  • アクセス順序オプション: 最近アクセスした順序で要素を並べることも可能
  • HashMap の性能特性: 基本操作の時間計算量は HashMap と同様
  • わずかなオーバーヘッド: 順序情報を維持するため、HashMap よりもわずかにメモリ使用量が多い
  • LRUキャッシュの実装に適している: アクセス順序モードと removeEldestEntry をオーバーライドすることで実現可能

LinkedHashMap の使用例

import java.util.LinkedHashMap;
import java.util.Map;

public class LinkedHashMapExample {
    public static void main(String[] args) {
        // 挿入順序を保持する LinkedHashMap
        Map<String, String> capitals = new LinkedHashMap<>();
        
        capitals.put("日本", "東京");
        capitals.put("フランス", "パリ");
        capitals.put("イタリア", "ローマ");
        
        // 挿入順序で走査される
        for (Map.Entry<String, String> entry : capitals.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        // 出力: 日本: 東京, フランス: パリ, イタリア: ローマ
        
        // アクセス順序を保持する LinkedHashMap (LRUキャッシュの基本実装)
        Map<String, String> lruCache = new LinkedHashMap<>(16, 0.75f, true);
        
        lruCache.put("A", "値A");
        lruCache.put("B", "値B");
        lruCache.put("C", "値C");
        
        // Bにアクセスすると、Bが最後に移動
        lruCache.get("B");
        
        // アクセス順序で走査: A, C, B
        for (String key : lruCache.keySet()) {
            System.out.println(key);
        }
        
        // 簡易LRUキャッシュの実装
        final int MAX_ENTRIES = 100;
        Map<String, String> lruCache2 = new LinkedHashMap<String, String>(MAX_ENTRIES, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
                return size() > MAX_ENTRIES;
            }
        };
    }
}

TreeMap

TreeMap は赤黒木に基づく Map インターフェースの実装で、キーが自然順序または指定された比較器に従ってソートされます。

TreeMap の特徴

  • ソート順序の保証: キーが常にソートされた状態で維持される
  • ナビゲーションメソッド: firstKey(), lastKey(), floorKey(), ceilingKey() などの便利なメソッドを提供
  • 操作の時間計算量: containsKey(), get(), put(), remove() は O(log n)
  • null キー非許容: 自然順序を使用する場合、null キーは許容されない
  • 比較可能なキー: キーは Comparable を実装するか、コンストラクタで Comparator を指定する必要がある

TreeMap の使用例

import java.util.Map;
import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        // 自然順序(アルファベット順)でソートされる TreeMap
        Map<String, Integer> scores = new TreeMap<>();
        
        scores.put("Charlie", 85);
        scores.put("Alice", 90);
        scores.put("Bob", 78);
        scores.put("David", 92);
        
        // キーがソートされた順序で走査される
        for (Map.Entry<String, Integer> entry : scores.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        // 出力: Alice: 90, Bob: 78, Charlie: 85, David: 92
        
        // TreeMap 固有のナビゲーションメソッド
        TreeMap<String, Integer> treeScores = (TreeMap<String, Integer>) scores;
        
        // 最初と最後のエントリ
        String firstStudent = treeScores.firstKey();  // "Alice"
        String lastStudent = treeScores.lastKey();    // "David"
        
        // 範囲検索
        Map<String, Integer> subMap = treeScores.subMap("Bob", "David");
        // Bob から David の直前までのエントリを含む
        
        // 指定されたキー以下/以上の最大/最小のキーを検索
        String floorKey = treeScores.floorKey("Chris");    // "Charlie"
        String ceilingKey = treeScores.ceilingKey("Chris"); // "David"
        
        // カスタム比較器を使用した TreeMap
        Map<String, Integer> reverseScores = new TreeMap<>((a, b) -> b.compareTo(a));
        reverseScores.putAll(scores);
        
        // 逆順で走査される
        for (String name : reverseScores.keySet()) {
            System.out.println(name);
        }
        // 出力: David, Charlie, Bob, Alice
    }
}

Hashtable

Hashtable は Java 1.0 から存在する古い実装で、HashMap と似ていますが、同期化されている(thread-safe)点とnull を許容しない点が異なります。

Hashtable の特徴と注意点

  • レガシーなクラス: 新しいコードでは ConcurrentHashMap や Collections.synchronizedMap() を使用することが推奨
  • 同期化: すべてのメソッドが同期化されているため、マルチスレッド環境で安全
  • null 非許容: キーと値の両方で null を許容しない
  • パフォーマンス: 同期化のオーバーヘッドにより、HashMap より低速

Map 実装の比較と選択ガイド

特性 HashMap LinkedHashMap TreeMap Hashtable
順序 なし 挿入順/アクセス順 ソート順 なし
パフォーマンス (get/put) O(1) O(1) O(log n) O(1)
null キー 許容 許容 非許容 非許容
null 値 許容 許容 許容 非許容
同期化 なし なし なし あり
メモリ使用量
イテレーション速度

Map 実装の選択ガイドライン

  • HashMap を選ぶ場合:
    • 最も一般的なユースケース
    • 順序が重要でない
    • 最高のパフォーマンスが必要
  • LinkedHashMap を選ぶ場合:
    • 挿入順序を保持する必要がある
    • LRUキャッシュを実装したい
    • 反復処理の順序が予測可能である必要がある
  • TreeMap を選ぶ場合:
    • キーをソートする必要がある
    • 範囲検索や最近接検索が必要
    • 順序付けられたキーセットが必要
  • ConcurrentHashMap を選ぶ場合:
    • マルチスレッド環境での高性能な並行アクセスが必要
    • Hashtable の代替として

効率的な Map 操作のためのテクニック

1. 初期容量と負荷係数の調整

HashMap や LinkedHashMap を使用する際、あらかじめ必要なエントリ数が分かっている場合は、初期容量を適切に設定することでリハッシュのオーバーヘッドを減らすことができます。

// 10,000エントリを格納することが分かっている場合
// デフォルトの負荷係数0.75を考慮して初期容量を設定
Map<String, User> userMap = new HashMap<>(13333); // 10000/0.75 ≈ 13333

2. キーの設計と equals/hashCode の実装

カスタムクラスをキーとして使用する場合、equals() と hashCode() メソッドを適切に実装することが重要です。

public class EmployeeKey {
    private final String department;
    private final int employeeId;
    
    public EmployeeKey(String department, int employeeId) {
        this.department = department;
        this.employeeId = employeeId;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        EmployeeKey that = (EmployeeKey) o;
        return employeeId == that.employeeId && 
               Objects.equals(department, that.department);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(department, employeeId);
    }
}

3. 効率的なマップの走査

Map を走査する際には、目的に応じて適切なメソッドを選択することが重要です。

Map<String, Integer> map = new HashMap<>();
// マップにデータを追加...

// キーと値の両方が必要な場合
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    String key = entry.getKey();
    Integer value = entry.getValue();
    // 処理...
}

// キーのみが必要な場合
for (String key : map.keySet()) {
    // 処理...
}

// 値のみが必要な場合
for (Integer value : map.values()) {
    // 処理...
}

// Java 8以降のラムダ式を使用した走査
map.forEach((key, value) -> {
    System.out.println(key + ": " + value);
});

4. 条件付き操作と集約操作

Java 8以降では、Map インターフェースに条件付き操作や集約操作のための便利なメソッドが追加されました。

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

// 条件付き操作
scores.putIfAbsent("Charlie", 85);  // キーが存在しない場合のみ追加
scores.computeIfPresent("Bob", (k, v) -> v + 5);  // 既存の値を更新
scores.computeIfAbsent("David", k -> 70);  // キーが存在しない場合に計算して追加

// マージ操作
scores.merge("Alice", 5, (oldValue, value) -> oldValue + value);  // 95 + 5 = 100

// 削除操作
scores.remove("Bob", 80);  // キーと値が一致する場合のみ削除
scores.replace("Charlie", 85, 90);  // 古い値が一致する場合のみ置換

Java 8以降の Stream API との連携

Java 8以降では、Map も Stream API と組み合わせて強力なデータ処理が可能になります。

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

public class MapStreamExample {
    public static void main(String[] args) {
        Map<String, Integer> scores = new HashMap<>();
        scores.put("Alice", 95);
        scores.put("Bob", 80);
        scores.put("Charlie", 85);
        scores.put("David", 70);
        
        // フィルタリング: 80点以上のエントリのみを抽出
        Map<String, Integer> highScores = scores.entrySet().stream()
                .filter(e -> e.getValue() >= 80)
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        Map.Entry::getValue
                ));
        // {Alice=95, Bob=80, Charlie=85}
        
        // 変換: すべてのスコアに10点を加算
        Map<String, Integer> bonusScores = scores.entrySet().stream()
                .collect(Collectors.toMap(
                        Map.Entry::getKey,
                        e -> e.getValue() + 10
                ));
        // {Alice=105, Bob=90, Charlie=95, David=80}
        
        // グループ化: スコア範囲でグループ化
        Map<String, List<String>> scoreGroups = scores.entrySet().stream()
                .collect(Collectors.groupingBy(
                        e -> {
                            int score = e.getValue();
                            if (score >= 90) return "A";
                            else if (score >= 80) return "B";
                            else if (score >= 70) return "C";
                            else return "D";
                        },
                        Collectors.mapping(Map.Entry::getKey, Collectors.toList())
                ));
        // {A=[Alice], B=[Bob, Charlie], C=[David]}
    }
}

マルチスレッド環境での Map の使用

複数のスレッドから同時にアクセスする場合は、同期化された Map 実装を使用する必要があります。

同期化された Map の作成方法

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

public class ThreadSafeMapExample {
    public static void main(String[] args) {
        // 方法1: Collections.synchronizedMap
        Map<String, Object> synchronizedMap = 
            Collections.synchronizedMap(new HashMap<>());
        
        // synchronizedMap を使用する場合、イテレーションには外部同期が必要
        synchronized (synchronizedMap) {
            for (String key : synchronizedMap.keySet()) {
                // 安全にアクセス
            }
        }
        
        // 方法2: ConcurrentHashMap (推奨)
        Map<String, Object> concurrentMap = new ConcurrentHashMap<>();
        
        // ConcurrentHashMap はイテレーション中の同期が不要
        for (String key : concurrentMap.keySet()) {
            // 安全にアクセス
        }
        
        // ConcurrentHashMap の並列処理機能
        concurrentMap.forEach(8, (key, value) -> 
            System.out.println(key + ": " + value)
        );
    }
}

まとめ

Map インターフェースは、キーと値のペアを効率的に格納・検索するための強力な抽象化を提供します。適切な実装を選択し、効率的な操作方法を理解することで、多くのプログラミング問題を効果的に解決できます。

  • HashMap: 最も一般的な実装で、高速なアクセスを提供します。順序が重要でない場合に適しています。
  • LinkedHashMap: 挿入順序またはアクセス順序を保持する必要がある場合に適しています。
  • TreeMap: キーをソートする必要がある場合や、範囲検索が必要な場合に適しています。
  • ConcurrentHashMap: マルチスレッド環境での並行アクセスが必要な場合に適しています。

多くの場合、HashMap がデフォルトの選択肢として適していますが、特定の要件(順序、ソート、スレッドセーフ)がある場合は、それに適した実装を選択してください。