Java : Map(マップ)の基本

Mapは、Java コレクション・フレームワークの1つです。

キーと値を一組の要素(Entry)として管理します。
大量の要素を追加しても、基本操作である getput のパフォーマンス低下が起こらないのが特徴です。

本記事では、そんな Map の基本的な使い方を解説していきます。


概要

キーを値にマッピングするオブジェクトです。 マップには、同一のキーを複数登録できません。各キーは1つの値にしかマッピングできません。

クラス構成

Map は、"キー" と "" を 一組の要素 として管理します。
この一組の要素のことを Entry とも呼びます。

マップ

Map に要素(Entry) を大量に追加
 ↓
キーを指定して値を高速に取得

というのが基本的な流れになります。

そんな Mapインタフェースには3つの大きな特徴があります。

  • キーの重複はできません。
  • (基本的には) 要素の順序は保持されません。
  • 大量の要素が追加されても、基本操作(put, getなど)は高速です。

基本操作

Map の生成

それでは、Map の基本的な使い方をコード例で見ていきましょう。

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

System.out.println(map); // {}
System.out.println(map.size()); // 0

まずは Map を生成します。

final Map<String, Integer> map = new HashMap<>();
                                 ^^^^^^^^^^^^^^^

今回は、Map の実装には HashMap を使いました。(HashMapについては後述します)

final Map<String, Integer> map = new HashMap<>();
          ^^^^^^  ^^^^^^^

Map には型パラメータが2つ必要です。

Map<K, V>の型パラメータには、

  • K : 要素のキーの型
  • V : 要素の値の型

を指定します。

今回は、キーの型に String、値の型に Integer を指定しました。

System.out.println(map); // {}
System.out.println(map.size()); // 0

Map にはまだ要素がないので、空である {} が表示されます。
サイズも 0 になります。

要素の追加

V put(K key, V value)
指定された値と指定されたキーをこのマップで関連付けます(オプションの操作)。

要素を1つ追加するには put メソッドを使います。
パラメータには キー を指定します。

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

System.out.println(map); // {}
System.out.println(map.size()); // 0

map.put("aaa", 10);

System.out.println(map); // {aaa=10}
System.out.println(map.size()); // 1

map.put("bbb", 20);

System.out.println(map); // {aaa=10, bbb=20}
System.out.println(map.size()); // 2

map.put("ccc", 30);

System.out.println(map); // {aaa=10, ccc=30, bbb=20}
System.out.println(map.size()); // 3

put するたびに、Mapに要素が追加されていきます。
サイズも増えていきます。

System.out.println(map); // {aaa=10, ccc=30, bbb=20}
                             ^^^^^^  ^^^^^^  ^^^^^^

注意点としては、HashMap の要素の順番は、要素が追加された順番になるとは限りません。
List とは違う点ですね。

値の取得

V get(Object key)
指定されたキーがマップされている値を返します。そのキーのマッピングがこのマップに含まれていない場合はnullを返します。

値を取得するには get メソッドを使います。
パラメータには キー を指定します。

final Map<String, Integer> map = new HashMap<>();
map.put("aaa", 10);
map.put("bbb", 20);
map.put("ccc", 30);

System.out.println(map); // {aaa=10, ccc=30, bbb=20}

final var ret1 = map.get("aaa");
System.out.println(ret1); // 10

final var ret2 = map.get("bbb");
System.out.println(ret2); // 20

final var ret3 = map.get("ccc");
System.out.println(ret3); // 30

指定したキーが存在しないときは、null が返ります。

final var ret4 = map.get("xxx");
System.out.println(ret4); // null

要素の削除

V remove(Object key)
このマップからキーのマッピング(ある場合)を削除します(オプションの操作)。

要素を削除するには remove メソッドを使います。
パラメータには キーを指定します。

final Map<String, Integer> map = new HashMap<>();
map.put("aaa", 10);
map.put("bbb", 20);
map.put("ccc", 30);

System.out.println(map); // {aaa=10, ccc=30, bbb=20}
System.out.println(map.size()); // 3

map.remove("ccc");

System.out.println(map); // {aaa=10, bbb=20}
System.out.println(map.size()); // 2

map.remove("aaa");

System.out.println(map); // {bbb=20}
System.out.println(map.size()); // 1

map.remove("bbb");

System.out.println(map); // {}
System.out.println(map.size()); // 0

すべての要素を取得

Set<Map.Entry<K,V>> entrySet()
このマップに含まれるマッピングのSetビューを返します。

すべての要素(Entry) を取得するには entrySet メソッドを使います。

要素は Map.Entry として取得できます。
さらに、キーを取得するには getKey メソッド、値を取得するには getValue メソッドを使います。

final Map<String, Integer> map = new HashMap<>();
map.put("aaa", 10);
map.put("bbb", 20);
map.put("ccc", 30);

System.out.println(map); // {aaa=10, ccc=30, bbb=20}

for (final var entry : map.entrySet()) {
    final var key = entry.getKey();
    final var value = entry.getValue();

    System.out.println("------");
    System.out.println("key : " + key);
    System.out.println("value : " + value);
}

// 結果
// ↓
//------
//key : aaa
//value : 10
//------
//key : ccc
//value : 30
//------
//key : bbb
//value : 20

その他のメソッド

他にも、Mapインタフェースには、

  • マップが空か判定 : isEmpty
  • 要素を一度に追加 : putAll
  • すべての要素を削除 : clear
  • すべてのキーを取得 : keySet

などなど、便利なメソッドがあります。

もう少し詳しく知りたい方は「Java : Map (マップ) - API使用例」の記事もご参照ください。

代表的なMapの実装

標準API には、Map の実装がいくつか用意されています。
用途に合わせて Map の実装を使い分けると、プログラムのパフォーマンスが向上します。

ここでは、よく使われる代表的な実装をご紹介します。

API 簡単な説明 要素の順序の保持 変更可能 nullキーを許容 null値を許容
HashMap もっとも基本となるMapの実装です。 ×
LinkedHashMap HashMapに順序の保持を追加した実装です。 〇 (要素の追加した順序が保持されます)
TreeMap 要素を常にソートした順序に保持する実装です。 〇 (ソートされた順序が保持されます) ×
Map.of 変更不可能なMapを返します。 × × × ×

HashMap

Mapインタフェースのハッシュ表に基づく実装です。
...
このクラスはマップの順序を保証しません。特に、その順序を常に一定に保つことを保証しません。
...
この実装は、ハッシュ関数が複数のバケットで要素を適切に分散させることを想定し、基本オペレーション(getおよびput)で一定時間の性能を提供します。

HashMap は、Map のもっとも基本となる実装です。
Object.hashCode でハッシュ表を作ることによって、Map の要素数が多くなっても基本操作(get, put)の性能は落ちません。

比較として List では要素数が多くなればなるほど、(末尾以外の)addremovecontains の性能は落ちていきます。

final var map = new HashMap<String, String>();

map.put("aaa", "あああ");
map.put("bbb", "いいい");
map.put("ccc", "ううう");
map.put("ddd", "えええ");

System.out.println(map); // {aaa=あああ, ccc=ううう, bbb=いいい, ddd=えええ}
System.out.println(map.size()); // 4

ハッシュ表の仕組み

とても簡易的にしたハッシュ表の仕組みをご紹介します。
どうしてハッシュ表は高速なのか?というのがなんとなくイメージできればな、と。

※Java の HashSetHashMap がこのような実装にはなっていないかもしれないのでご注意ください。(もう少し効率よくなっているとは思います)

ハッシュ表には、その名のとおりハッシュコードを使います。
Mapでは要素のキーのハッシュを使います。

Object.hashCodeint だと範囲が広いので、今回は分かりやすく 0 ~ 99 の範囲のハッシュ値に変換します。
もちろん、ハッシュコードの範囲を狭くすることで衝突しやすくなります…が、そのあたりは今回は考えないことにします。

// 追加するキーを4つ用意します。
final var keys = List.of("aaa", "bbb", "ccc", "ddd");

// それぞれのハッシュコード
for (final var key : keys) {

    //aaa : hashCode = 96321
    //bbb : hashCode = 97314
    //ccc : hashCode = 98307
    //ddd : hashCode = 99300
    System.out.println(key + " : hashCode = " + key.hashCode());
}

// ハッシュコードの範囲を0~99にします。
for (final var key : keys) {

    //aaa : hashCode = 21
    //bbb : hashCode = 14
    //ccc : hashCode = 7
    //ddd : hashCode = 0
    System.out.println(key + " : hashCode = " + Math.abs((key.hashCode() % 100)));
}

ハッシュコードは 0~99 の範囲となるので、ハッシュ表にはサイズ100の配列を用意します。
そして、Map.put で要素(キーと値)を追加するときに、

  1. キーのハッシュコード(0~99)を計算
  2. 配列のインデックス(添え字)に、1. で計算したハッシュコードを使う
  3. 配列[キーのハッシュコード] に、要素の値を格納

という処理をします。

ハッシュ表

put, get は、単純な配列へのインデックス参照となるので、高速になる、というのはイメージできますでしょうか。
その代わり、サイズ100の配列を作ったのに使われているのは4箇所だけです。残りの96箇所は無駄になっていますね。

メモリは余計に使うけど、処理速度を優先させたもの…それがハッシュ表です。

もちろん、実際の実装はもっと効率良くなっているはずです。
ハッシュコードが衝突することも考慮されています。

本記事では、そのあたりの難しいところは割愛します。

LinkedHashMap

予測可能な反復順序を持つMapインタフェースのハッシュ表とリンク・リストの実装です。 この実装は、すべてのエントリをまたがる二重のリンク・リストを保持するという点で、HashMapとは異なります。 このリンク・リストは、反復順序を定義します。この順序は通常、キーがマップに挿入された順序です(挿入順)。

Map は基本的に要素の順序を保持しません。
しかし、LinkedHashMap は順序を保持する Map です。

// LinkedHashMapの例
final var linkedHashMap = new LinkedHashMap<String, Integer>();

linkedHashMap.put("aaa", 10);
linkedHashMap.put("bbb", 20);
linkedHashMap.put("ccc", 30);
linkedHashMap.put("ddd", 40);

// 要素の順番は、要素が追加された順になります。
System.out.println(linkedHashMap); // {aaa=10, bbb=20, ccc=30, ddd=40}

// HashMapの例
final var hashMap = new HashMap<String, Integer>();

hashMap.put("aaa", 10);
hashMap.put("bbb", 20);
hashMap.put("ccc", 30);
hashMap.put("ddd", 40);

System.out.println(hashMap); // {aaa=10, ccc=30, bbb=20, ddd=40}

TreeMap

赤 - 黒ツリーに基づくNavigableMap実装です。 マップは、使用するコンストラクタに応じて、そのキーの自然順序付けに従って、またはマップ作成時に提供されるComparatorによってソートされます。

TreeMap は、要素の並び順を、要素のキーで常にソートしてくれる Map です。

final var map = new TreeMap<String, Integer>();

map.put("ccc", 10);
map.put("ddd", 20);
map.put("bbb", 30);
map.put("aaa", 40);

System.out.println(map); // {aaa=40, bbb=30, ccc=10, ddd=20}
System.out.println(map.size()); // 4

Map.of

static <K,​ V> Map<K,​V> of​(K k1, V v1, K k2, V v2, K k3, V v3, K k4, V v4)
4つのマッピングを含む変更不可能なマップを返します。

Map.of変更不可能(イミュータブル) な Map を返します。
Map を変更するメソッド(putremove など)を使うと、非チェック例外の UnsupportedOperationException が発生します。

final var map = Map.of(
        "aaa", 10,
        "bbb", 20,
        "ccc", 30,
        "ddd", 40);

System.out.println(map); // {ddd=40, aaa=10, bbb=20, ccc=30}
System.out.println(map.size()); // 4

//map.put("eee", 50); // UnsupportedOperationException発生
//map.remove("aaa"); // UnsupportedOperationException発生

独自クラスをキーにする

独自クラスを Map のキーに指定したい場合は、その独自クラスの hashCodeequals メソッドを正しくオーバーライドしましょう。
hashCodeequals メソッドのオーバーライドについては、

の記事でも解説していますので、そちらもご参照ください。

コード例になります。

public class Sample {
    private final String text;
    private final int value;

    public Sample(String text, int value) {
        this.text = text;
        this.value = value;
    }

    public String getText() {
        return text;
    }

    public int getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Sample sample = (Sample) o;
        return value == sample.value && Objects.equals(text, sample.text);
    }

    @Override
    public int hashCode() {
        return Objects.hash(text, value);
    }

    @Override
    public String toString() {
        return "Sample[text=" + text + ", value=" + value + "]";
    }
}
final Map<Sample, String> map = Map.of(
        new Sample("aaa", 1), "xxx",
        new Sample("bbb", 2), "yyy",
        new Sample("ccc", 3), "zzz");

// {Sample[text=bbb, value=2]=yyy, Sample[text=ccc, value=3]=zzz, Sample[text=aaa, value=1]=xxx}
System.out.println(map);

もしくは、Java 16 以降を使っているのであれば、レコードクラスが便利です。
レコードクラスでは、面倒な hashCodeequals のオーバーライドを自動でしてくれます。

public record Sample(String text, int value) {
}
final Map<Sample, String> map = Map.of(
        new Sample("aaa", 1), "xxx",
        new Sample("bbb", 2), "yyy",
        new Sample("ccc", 3), "zzz");

// {Sample[text=bbb, value=2]=yyy, Sample[text=ccc, value=3]=zzz, Sample[text=aaa, value=1]=xxx}
System.out.println(map);

非常にシンプルになりました。

レコードクラスについては

の記事も参考にしていただけたら幸いです。

Dictionaryクラスは使わない

ノート: このクラスは現在使われていません。 新しい実装では、このクラスを拡張しないでMapインタフェースを実装してください。

Map と似たAPIとして Dictionary クラス(とそのサブクラス)があります。
しかし、API仕様にもあるように、すでに使われていません。

Dictionaryクラスは使わずに、Mapインタフェースを代わりに使いましょう。

まとめ

Mapインタフェースには3つの大きな特徴があります。

  • キーの重複はできません。
  • (基本的には) 要素の順序は保持されません。
  • 大量の要素が追加されても、基本操作(put, getなど)は高速です。

また、Mapの主な実装には

  • HashMap
  • LinkedHashMap
  • TreeMap
  • Map.of

があります。

用途によってうまく使い分けるように心がけたいですね。


関連記事

ページの先頭へ