Java : Map(マップ)の基本
Mapは、Java コレクション・フレームワークの1つです。
キーと値を一組の要素(Entry)として管理します。
大量の要素を追加しても、基本操作である get や put のパフォーマンス低下が起こらないのが特徴です。
本記事では、そんな Map の基本的な使い方を解説していきます。
概要
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 になります。
要素の追加
要素を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 とは違う点ですね。
値の取得
値を取得するには 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
要素の削除
要素を削除するには 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
すべての要素を取得
すべての要素(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
HashMap は、Map のもっとも基本となる実装です。
Object.hashCode でハッシュ表を作ることによって、Map の要素数が多くなっても基本操作(get, put)の性能は落ちません。
比較として List では要素数が多くなればなるほど、(末尾以外の)add や remove、contains の性能は落ちていきます。
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 の HashSet や HashMap がこのような実装にはなっていないかもしれないのでご注意ください。(もう少し効率よくなっているとは思います)
ハッシュ表には、その名のとおりハッシュコードを使います。
Mapでは要素のキーのハッシュを使います。
Object.hashCode の int だと範囲が広いので、今回は分かりやすく 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 で要素(キーと値)を追加するときに、
- キーのハッシュコード(0~99)を計算
- 配列のインデックス(添え字)に、1. で計算したハッシュコードを使う
- 配列[キーのハッシュコード] に、要素の値を格納
という処理をします。
put, get は、単純な配列へのインデックス参照となるので、高速になる、というのはイメージできますでしょうか。
その代わり、サイズ100の配列を作ったのに使われているのは4箇所だけです。残りの96箇所は無駄になっていますね。
メモリは余計に使うけど、処理速度を優先させたもの…それがハッシュ表です。
もちろん、実際の実装はもっと効率良くなっているはずです。
ハッシュコードが衝突することも考慮されています。
本記事では、そのあたりの難しいところは割愛します。
LinkedHashMap
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
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
Map.of は 変更不可能(イミュータブル) な Map を返します。
Map を変更するメソッド(put や remove など)を使うと、非チェック例外の 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 のキーに指定したい場合は、その独自クラスの hashCode と equals メソッドを正しくオーバーライドしましょう。
hashCode と equals メソッドのオーバーライドについては、
の記事でも解説していますので、そちらもご参照ください。
コード例になります。
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 以降を使っているのであれば、レコードクラスが便利です。
レコードクラスでは、面倒な hashCode と equals のオーバーライドを自動でしてくれます。
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 と似たAPIとして Dictionary クラス(とそのサブクラス)があります。
しかし、API仕様にもあるように、すでに使われていません。
Dictionaryクラスは使わずに、Mapインタフェースを代わりに使いましょう。
まとめ
Mapインタフェースには3つの大きな特徴があります。
- キーの重複はできません。
- (基本的には) 要素の順序は保持されません。
- 大量の要素が追加されても、基本操作(put, getなど)は高速です。
また、Mapの主な実装には
- HashMap
- LinkedHashMap
- TreeMap
- Map.of
があります。
用途によってうまく使い分けるように心がけたいですね。
関連記事
- Object.hashCode (ハッシュ・コード) とは
- レコードクラスの基本 (Record Class)
- 不変オブジェクト(イミュータブル) とは
- List(リスト)の基本
- Set(セット)の基本
- 配列 vs. List
- 配列からListへの変換
- List から配列への変換
- API 使用例
- Collection (コレクション)
- Collections (コレクション操作)
- Comparable
- Comparator
- Iterator
- List (リスト)
- Map (マップ)
- Map.Entry (キーと値のペア)
- Queue (キュー)
- AbstractQueue
- ArrayBlockingQueue (ブロッキング・配列キュー)
- ArrayDeque (両端キュー)
- BlockingDeque (ブロッキング・両端キュー)
- BlockingQueue (ブロッキング・キュー)
- ConcurrentLinkedDeque (並列処理用・両端キュー)
- ConcurrentLinkedQueue (並列処理用キュー)
- Deque (両端キュー)
- LinkedBlockingDeque (ブロッキング・リンク両端キュー)
- LinkedBlockingQueue (ブロッキング・リンクキュー)
- LinkedList (二重リンク・リスト)
- PriorityBlockingQueue (ブロッキング・優先度付きキュー)
- PriorityQueue (優先度付きキュー)
- RandomAccess
- Set (セット)
- Spliterator