7.

Java の Map(連想配列)|HashMap と LinkedHashMap の使い分け

編集
この記事の要点
  • Mapインターフェース。実装は HashMap / LinkedHashMap / TreeMap などから選ぶ
  • HashMap の順序は保証されない。登録順が要るなら LinkedHashMap
  • キーにするクラスは equals()hashCode() の両方を実装する
  • 無いキーを get() すると nullgetOrDefault() を使えば分岐が減る
  • 集計は computeIfAbsent()merge()containsKey + put より速く短い

作り方と基本操作

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

scores.put("math", 80);
scores.put("english", 70);
scores.put("math", 90);              // 同じキーなら上書き(戻り値は前の値 80)

System.out.println(scores.get("math"));            // 90
System.out.println(scores.get("science"));         // null
System.out.println(scores.getOrDefault("science", 0));  // 0

System.out.println(scores.containsKey("math"));    // true
System.out.println(scores.containsValue(70));      // true
System.out.println(scores.size());                 // 2
System.out.println(scores.isEmpty());              // false

scores.remove("english");
scores.clear();

// 変更できない Map を作る(Java 9 以降)
Map<String, Integer> fixed = Map.of("math", 80, "english", 70);
// fixed.put("x", 1);                // UnsupportedOperationException

// 10 組を超えるときや順序を保ちたいとき
Map<String, Integer> fixed2 = Map.ofEntries(
    Map.entry("math", 80),
    Map.entry("english", 70));

// 既存の Map から作る
Map<String, Integer> copy = new HashMap<>(scores);
Map<String, Integer> readOnly = Collections.unmodifiableMap(scores);

実装クラスの使い分け

クラス順序速度null キー使う場面
HashMap不定O(1)1 個だけ可既定
LinkedHashMap登録順O(1)1 個だけ可順序を保ちたい / LRU キャッシュ
TreeMapキーの昇順O(log n)不可並べ替えて持ちたい / 範囲検索
ConcurrentHashMap不定O(1)不可複数スレッドから使う
EnumMapenum の定義順非常に速い不可キーが enum のとき
Hashtable不定遅い不可使わない(古い)
Map<String, Integer> hash = new HashMap<>();
hash.put("c", 3); hash.put("a", 1); hash.put("b", 2);
System.out.println(hash);        // {a=1, b=2, c=3} と出ることもあるが保証はない

Map<String, Integer> linked = new LinkedHashMap<>();
linked.put("c", 3); linked.put("a", 1); linked.put("b", 2);
System.out.println(linked);      // {c=3, a=1, b=2}   登録順

Map<String, Integer> tree = new TreeMap<>(hash);
System.out.println(tree);        // {a=1, b=2, c=3}   キーの昇順
System.out.println(((TreeMap<String,Integer>) tree).firstKey());   // a

HashMap の並び順に依存したコードを書かないでください。要素数や JDK のバージョンで変わります。

キーの条件

public class UserKey {
    private final int id;
    public UserKey(int id) { this.id = id; }
}

Map<UserKey, String> map = new HashMap<>();
map.put(new UserKey(1), "田中");
System.out.println(map.get(new UserKey(1)));   // null   ← 見つからない
// equals と hashCode を実装すれば見つかる
public record UserKey(int id) {}      // record なら自動生成される

Map<UserKey, String> map = new HashMap<>();
map.put(new UserKey(1), "田中");
System.out.println(map.get(new UserKey(1)));   // 田中

キーに使うオブジェクトは、登録後に中身を変えてはいけません。hashCode() が変わると、入れたはずの値が二度と取り出せなくなります。recordfinal フィールドで不変にしてください。

集計に使う

List<String> words = List.of("a", "b", "a", "c", "a");

// 出現回数を数える(古い書き方)
Map<String, Integer> count = new HashMap<>();
for (String w : words) {
    if (count.containsKey(w)) {
        count.put(w, count.get(w) + 1);
    } else {
        count.put(w, 1);
    }
}

// merge を使う
Map<String, Integer> count2 = new HashMap<>();
for (String w : words) {
    count2.merge(w, 1, Integer::sum);
}

// getOrDefault を使う
count2.put("a", count2.getOrDefault("a", 0) + 1);

// キーごとにリストへ追加する
Map<String, List<String>> grouped = new HashMap<>();
for (String w : words) {
    grouped.computeIfAbsent(w, k -> new ArrayList<>()).add(w);
}

// Stream で一気に
Map<String, Long> count3 = words.stream()
    .collect(Collectors.groupingBy(w -> w, Collectors.counting()));
メソッド働き
putIfAbsent(k, v)キーが無い(または null)ときだけ入れる
computeIfAbsent(k, f)無ければ f で作って入れ、その値を返す
computeIfPresent(k, f)あるときだけ計算し直す
compute(k, f)有無に関わらず計算する。null を返すと削除される
merge(k, v, f)無ければ v、あれば f(既存, v)
replaceAll(f)すべての値を変換する

走査する

Map<String, Integer> scores = Map.of("math", 80, "english", 70);

// キーと値を両方使う(最も効率が良い)
for (Map.Entry<String, Integer> e : scores.entrySet()) {
    System.out.println(e.getKey() + ": " + e.getValue());
}

// ラムダ
scores.forEach((k, v) -> System.out.println(k + ": " + v));

// キーだけ / 値だけ
for (String k : scores.keySet()) { }
for (Integer v : scores.values()) { }

// 走査しながら削除するときは Iterator を使う
Iterator<Map.Entry<String, Integer>> it = new HashMap<>(scores).entrySet().iterator();
while (it.hasNext()) {
    if (it.next().getValue() < 75) it.remove();
}
// または
Map<String, Integer> m = new HashMap<>(scores);
m.entrySet().removeIf(e -> e.getValue() < 75);

拡張 for の中で put()remove() を呼ぶと ConcurrentModificationException になります。取り出しの詳細は キーと値の取得 を参照してください。

null の扱い

Map<String, Integer> map = new HashMap<>();
map.put("a", null);

System.out.println(map.get("a"));          // null
System.out.println(map.get("missing"));    // null   ← 区別できない
System.out.println(map.containsKey("a"));  // true   こちらで区別する

// アンボックス化で落ちる
// int n = map.get("missing");             // NullPointerException
int n = map.getOrDefault("missing", 0);    // 安全

// Map.of は null を許さない
// Map.of("a", null);                      // NullPointerException

スレッドと性能

// 複数スレッドから触るなら ConcurrentHashMap
Map<String, Integer> safe = new ConcurrentHashMap<>();
safe.merge("a", 1, Integer::sum);      // これ自体は原子的

// 件数が分かっているなら初期容量を指定する(再構築を避ける)
Map<String, Integer> big = new HashMap<>(10_000);

// LinkedHashMap で LRU キャッシュ
Map<String, String> lru = new LinkedHashMap<>(16, 0.75f, true) {
    @Override protected boolean removeEldestEntry(Map.Entry<String, String> e) {
        return size() > 100;
    }
};

HashMap を複数スレッドから同時に更新すると、無限ループやデータの消失が起きます。共有するなら ConcurrentHashMap にしてください。

関連

編集
Post Share
子ページ
  1. キーと値の取得
同階層のページ
  1. 基本的なルール
  2. データ型
  3. 変数
  4. 定数
  5. 配列
  6. コレクション(List,Set,Queue)
  7. Map(連想配列)
  8. 演算子
  9. 条件分岐
  10. 繰り返し制御文
  11. クラス
  12. メソッド
  13. インスタンス化
  14. コンストラクタ
  15. staticキーワード
  16. オーバーロード
  17. 継承
  18. オーバーライド
  19. this
  20. super
  21. パッケージ
  22. アクセス修飾子
  23. 抽象クラス・メソッド
  24. インターフェース
  25. カプセル化
  26. データベース接続
  27. セッション
  28. ファイル入出力
  29. ラムダ式

最近更新/作成されたページ