JavaのMapとは?キーと値を対応付けて保存するためのMapの使い方
Mapは、Javaでキーと値をセットにして扱いたい時に使うデータ構造です。
他のプログラミング言語で、ハッシュテーブル・ディクショナリ・連想配列などと呼ばれるものに近い使い方ができるものです。
Mapの機能はシンプルですが、応用範囲はとても広く、Javaで実用的なプログラムを作る時は大体お世話になるものです。
だからこそ、Mapの使い方をよく知っていれば、プログラミングをとても楽に、効率的にできるようになります。
逆に言えば、Mapの使い方を誤れば、プログラムがとても読みにくい、使いにくいものになってしまいかねません。
でも、いくつかの基礎的な知識やポイントを押さえるだけで、Mapの力を生かしたプログラミングは十分に行えるのです。
この記事では、Mapの特徴を理解するところから始めて、Mapの基本的なメソッドの使い方と実用的な活用の仕方、知っておけばちょっと得するワンポイント的なことまで、幅広くお伝えします。
※この記事のサンプルは、Java 11の環境で動作確認しています
目次
1.Mapとはどんなものか
1-1.Mapはキーと値を対応付けて保存するモノ
マップ(map)という言葉で最初に連想するのは、普通は「地図」でしょうか。でも、マッピングするという言い回しもしますよね。こちらは「対応付ける」という意味です。
Javaのjava.util.Mapは、その「対応付ける」方のmapをJavaで実現したものです。つまり、プログラム上で「何かを何かに対応付け」て、それを一つのMapのインスタンスでひとまとめにして、プログラム中で自由に持ち運びができるのです。
Mapの登場人物は、Mapそのものの他に、「キー」となる何かのクラスのインスタンスと、キーに対応付ける「値」の二つです。Mapはキーと値をセットにして覚えるので、同じキーなら常に同じ値が得られます。そして、一つのMapにはたくさんのキーを一度に対応付けられるのです。
1-2.Mapはできることが決まっているインターフェイス
java.util.Mapはインターフェイスです。つまり、Mapというモノができる振る舞い(=メソッド)を決めたものです。そして、Mapの実体となる、Mapのインターフェイスを実装したクラスが用途別に作られているので、しっかりと使い分けなければならない…ということでもあります。
Mapができる代表的な振る舞いは以下のものです。
- キーと値を対応付けて保存する(put)
- キーに対応付けた値を取得する(get)
- キーを削除する(remove)
- キーをすべて削除する(clear)
- キーがあるか確認する(containsKey)
- 値があるか確認する(containsValue)
- キーを全部取得する(keySet)
- 値を全部取得する(values)
- 対応付けたキーと値を全部取得する(entrySet)
1-3.Mapができること、他のデータ構造との違い
Javaでは、Mapの他にもいろいろなデータ構造が使えます。それらとMapの違いは以下のとおりです。Mapの特徴は、キーを使ったデータの間接的・構造的な管理ができることなので、その特徴を活かして使いたいですね。
データ構造 | できること | 得意 | 苦手 |
Map | キーと値を対応付けた保持、キーを中心にした操作 | 特定のキーの有無を高速に調べる、キーに対する値への高速なアクセス | 値を中心とした操作、特定の値の有無を高速に調べる |
List | 確保領域のサイズを意識せずに値を溜める、インデックスを中心とした操作 | データの単純追加、溜めた順番どおりの値の取出し、インデックスを使った高速なランダムアクセス | 特定の値の有無や存在先のインデックスを高速に調べる、値を途中へ挿入する(得意な実装クラスもある) |
Set | 値を重複させずに溜める、値を中心にした操作 | 特定の値の有無を高速に調べる、値の重複チェック | インデックスを使った操作(できない)、溜める順番どおりの処理(得意な実装クラスもある) |
Queue | ルールに従った値の出し入れをロジックを意識せずにできる(先入れ先出し、後入れ先出しなど) | ルールに従った値の出し入れ | インデックスを使った操作(できない)、値を途中へ挿入する(できない) |
配列 | 事前に確保した固定長領域へのインデックスによるアクセス | インデックスを使った高速なランダムアクセス、多次元構造の容易な表現 | 可変長領域を扱う、値を途中に挿入する(移し替えが必要)、メモリ使用量の最適化(扱う型やサイズ、状況にもよる) |
クラス(自作) | クラスを作ったプログラマの意図どおりに何でもできる | メソッドによる操作への意味付け、処理内容の最適化、要件に応じた値の自由な保持・加工 | 作る上でのお手軽さ、メモリ使用量の最適化、各種チューニング(プログラマが全て自分で行う) |
2.【基本】Mapの使い方(~Java 7)
この章ではMapの基本的な使い方を紹介します。Mapの実装クラスとしてHashMapを使いますが、他のMapの実装クラスでも使い方は原則同じです。
この章で紹介するMapのメソッドは、Java 7までのMapで同じように使えます。JavaはJava 8で大きく機能が強化され、Mapも例外ではありません。でも、プログラミングの現場ではまだJava 7以前を使うこともありますし、Java 7までのMapの使い方は基礎でもありますので、ぜひマスターしましょう。
Java 8以降でMapに追加されたメソッドの使い方は別の章に記載しましたので、ぜひお読みください。色々なことが簡単にできるようになっていますよ。
2-1.【重要】Mapでは型引数でキーと値を指定しよう!!
この記事の例では、Mapへの型引数を使います。型引数についてご存じでない方向けに、簡単に説明します。
型引数とは、MapのAPIドキュメント(Javadoc)でKやVと書かれているものです。Kがキーとなるクラス(KeyのK)、Vが値となるクラス(ValueのV)のことです。何も指定しないと、KもVもどんなクラスでもあてはまるObjectになりますが、これをプログラマが自由に指定できるのです。
2-1-1.Mapでの型引数の使い方
Mapの変数宣言の<>でくくられた部分を使って、そのMapのKやVが実際に何かを指定します。型引数を使ってMapの変数を宣言すると、キーと値には指定したクラスしか使えないように、コンパイラがチェックできるようになります。
例えば「Map<String, Integer>」なら、最初のStringはキーのクラスで、次のIntegerは値のクラスです。以下のように、色々なパターンで指定ができます。それに少々ややこしいですが、型引数の中に型引数を持つクラスを指定する…というような、ネストした型引数も使えるのです。
// 変数を宣言する時、Map<K, V>のKとVに何を書くかで、Mapで扱えるキーと値を指定できる Map map0; // → キーはObject、値もObject(Java 1.4までの書き方) Map<Object, Object> map1; // → キーはObject、値もObject Map<String, Integer> map2; // → キーはString、値はInteger Map<Integer, String> map3; // → キーはInteger、値はString Map<String, List<String>> map4; // → キーはString、値はStringを持つList Map<String, Map<Integer, Object>> map5; // → キーはString、値はキーにInteger・値にObjectを持つMap Map<?, ?> map6; // → キーはなんでもいい、値もなんでもいい
2-1-2.型引数を使ったMapは使いやすい
型引数を指定すれば、Mapがキーや値として何を扱うのか、見ればすぐにわかります。それに、値をgetする時にキャストがいりませんし、型引数で指定した以外のクラスをキー・値としてputしようとすると、コンパイラがエラーにしてくれます。ですから、プログラムの品質向上にも大きく繋がるのです!!
Map map0 = new HashMap(); // → キーはObject、値もObject(Java 1.4までの書き方) Map<Object, Object> map1 = new HashMap(); // → キーはObject、値もObject Map<String, Integer> map2 = new HashMap(); // → キーはString、値はInteger List value0 = (List) map0.get("キー"); // 本当に値がListかは、動かさないとわからない!! String value1 = (String) map1.get("キー"); // 値がObjectだと、実際に入っているクラスへのキャストが必要 Integer value2 = map2.get("キー"); // 型引数で値のクラスが指定できていれば、キャストがいらない!! map2.put("キー", "値"); // → コンパイルエラー、値にIntegerではないStringが指定されているため
2-2.putでキーと値を対応付ける
Map.putを使うと、Mapへキーと値を対応付けられます。既にキーがMapにある場合は、新しい対応付けで上書きされます。キーや値としてnullが使えるかは、Mapの実装クラス次第ですので注意しましょう。
putでキーとするクラスは、少なくともequalsがきちんと動作するように作られていなければなりません。そうでなければ、キーが同じかどうかをMapが判断できず、getなどMap全体の動作に影響してしまいます。
// Java 11の日本語版Javadocより抜粋 V put(K key, V value) パラメータ: key - 指定された値が関連付けられるキー value - 指定された鍵に関連付けられる値 戻り値: keyに以前に関連付けられていた値。keyのマッピングが存在しなかった場合はnull。
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); // → "猫" に "マイケル" を対応付ける map.put("犬", "伸之助"); // → "犬" に "伸之助" を対応付ける String name = map.put("猫", "ニャジラ"); // → このMapの"猫"に対応付けられたものを"ニャジラ"に変える System.out.println(name); // → "マイケル"、入れ替える前の値
2-3.getでキーに対応付けられた値を取り出す
Map.getを使うと、Mapにputされているキー・値の対応付けの中から、キーに対応する値を取り出せます。キーがputされていない場合はnullが戻ります。
ここで「取り出す」という表現をしましたが、getしてもMapのキーと値の対応付けはそのままです。なぜかと言うと、getで戻ってくるのはMapが値として持つインスタンスへの参照だからです。つまり、getはMapへ「キーに対応する値をちょっと見せてよ」とお願いするようなニュアンスの操作です。
// Java 11の日本語版Javadocより抜粋 V get(Object key) パラメータ: key - 関連付けられた値が返される鍵 戻り値: 指定されたキーがマップされている値。そのキーのマッピングがこのマップに含まれていない場合はnull
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); String name1 = map.get("猫"); System.out.println(name1); // → "マイケル" String name2 = map.get("狸"); // "狸" というキーはmapに存在しないが、エラーにはならない System.out.println(name2); // → null
2-4.removeでキーを削除する
Map.removeを使うと、Mapにputされているキーを削除します。キーを削除する時、キーに対応付けられた値がもしあれば、Mapからいっしょに削除されます。キーがputされていなくてもエラーにはなりません。putされていたかは、戻り値で分かります。
キーはMapにあるままとして、対応付けられている値だけを削除したいのなら、同じキーを使ってnullでputし直します。ただし、値にnullが使えるかはMapの実装クラス次第ですので、注意しましょう。
// Java 11の日本語版Javadocより抜粋 V remove(Object key) パラメータ: key - マッピングがマップから削除されるキー 戻り値: keyに以前に関連付けられていた値。keyのマッピングが存在しなかった場合はnull。
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); String name1 = map.remove("猫"); // mapから"猫"を削除する System.out.println(name1); // → "マイケル"、削除時には"マイケル"が対応付けられていた String name2 = map.get("猫"); System.out.println(name2); // → null、キーがremoveされているのでgetしてもnullになる String name3 = map.remove("狸"); // "狸" というキーはmapに存在しないが、エラーにはならない System.out.println(name3); // → null
2-5.clearですべてのキーを削除する
Map.clearを使うと、Mapにputされているキーをすべて削除します。Mapを空にしたい時に、キーを一つ一つremoveしては時間もかかりますし、プログラミングも大変です。clearを使って楽をしましょう。
// Java 11の日本語版Javadocより抜粋 void clear() マップからマッピングをすべて削除します(オプションの操作)。この呼出しが戻ると、マップは空になります。
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); map.clear(); // mapからすべてのキーを削除する String name1 = map.get("猫"); System.out.println(name1); // → null、mapからキーが削除された String name2 = map.get("犬"); System.out.println(name2); // → null、同上
2-6.キーや値があるか確認する
2-6-1.isEmptyで空っぽか確認する
Map.isEmptyを使うと、Mapにキーがあるかbooleanで分かります。キーと値の対応付けが一つでもあるならtrue、ないならfalseです。keySetで戻ってくるSetのsizeでもいいのですが、こちらの方が行っていることの意味が明確になりますのでお勧めです。
// Java 11の日本語版Javadocより抜粋 boolean isEmpty() 戻り値: このマップがキーと値のマッピングを保持しない場合はtrue
Map<String, String> map = new HashMap<>(); boolean isEmpty = map.isEmpty(); System.out.println(isEmpty); // → true map.put("猫", "マイケル"); // mapにキーを追加 map.put("犬", "伸之助"); isEmpty = map.isEmpty(); System.out.println(isEmpty); // → false map.clear(); // mapからすべてのキーを削除する isEmpty = map.isEmpty(); System.out.println(isEmpty); // → true
2-6-2.containsKeyでキーがあるか確認する
Map.containsKeyで、Mapにキーがあるか確認できます。getして値がnullか確認するやり方でもいいのですが、値がnullだった場合にキーの有無の区別がつきません。Mapがキーを持っているかを「確実に」判断したいのなら、containsKeyを使いましょう。
// Java 11の日本語版Javadocより抜粋 boolean containsKey(Object key) パラメータ: key - このマップ内にあるかどうかが判定されるキー 戻り値: 指定されたキーのマッピングがこのマップに含まれている場合はtrue
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); boolean contains1 = map.containsKey("猫"); System.out.println(cotnains1); // → true、キー"猫"はmapにある boolean contains2 = map.containsKey("犬"); System.out.println(cotnains2); // → true、キー"犬"はmapにある boolean contains3 = map.containsKey("狸"); System.out.println(cotnains3); // → false、キー"狸"はmapにない
2-6-3.containsValueで値があるか確認する
Map.containsValueで、Mapに値があるか確認できます。containsValueの引数で渡したインスタンスと、equalsでtrueになる値が少なくとも一つあるかを調べます。ですから、値に使うクラスがequalsをきちんと作ってあるかがとても大事です。
// Java 11の日本語版Javadocより抜粋 boolean containsValue(Object value) パラメータ: value - このマップにあるかどうかが判定される値 戻り値: このマップが1つまたは複数のキーを指定された値にマッピングしている場合はtrue
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); map.put("ポップスの帝王", "マイケル"); boolean contains1 = map.containsValue("マイケル"); System.out.println(cotnains1); // → true、値"マイケル"はmapにある boolean contains2 = map.containsValue("伸之助"); System.out.println(cotnains2); // → true、値"伸之助"はmapにある boolean contains3 = map.containsValue("ニャジラ"); System.out.println(cotnains3); // → false、値"ニャジラ"はmapにない
2-7.キーや値をSetやCollectionで取り出す
Mapの外部から、持っているキーや値の一覧を自由に取り出せます。そうして取り出した結果はSetやCollectionとなるのですが、取り出した元のMapと連動しているので操作する時は注意が必要です。どんな感じで連動しているかは、別の章でお伝えします。
2-7-1.keySetでキーをすべて取得する
Map.keySetを使うと、Mapが持っているキーをすべて値として含むSetを得られます。キーでループをしたい場合などに使います。キーの数を知りたいなら、結果のSetのメソッドsizeを使うといいでしょう。
// Java 11の日本語版Javadocより抜粋 Set<K> keySet() 戻り値: マップに含まれているキーのセット・ビュー
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); Set<String> keys = map.keySet(); System.out.println(keys.size()); // → 2、mapにはキーが2つあるため System.out.println(keys); // → [猫, 犬]
2-7-2.valuesで値をすべて取得する
Map.valuesを使うと、Mapが持っている値をすべて含むCollectionを得られます。値だけが欲しい場合などに使います。キーの数を知りたいなら、結果のCollectionのメソッドsizeを使うといいでしょう。
このメソッドを使う時に意識したいのは、戻り値は持っている要素が一意になるSetではなく、ただの要素の集まりであるCollectionだということです。つまり、異なるキーで同じ値を含んでいても、戻り値のCollectionには違う要素として含まれます。
// Java 11の日本語版Javadocより抜粋 Collection<V> values() 戻り値: マップ内に含まれている値のコレクション・ビュー
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); map.put("ポップスの帝王", "マイケル"); Collection<String> values = map.values(); System.out.println(values.size()); // → 3、mapには値が「3つ」あるため System.out.println(values); // → [マイケル, マイケル, 伸之助]
2-7-3.entrySetでMapが持つキー・値をすべて取得する
Map.entrySetを使うと、キーと値をペアで持つMapの内部クラスMap.EntryのSetを得られます。keySetやvaluesはキー・値だけでしたが、こちらのメソッドでは両方一度に得られるのが異なるところです。
Map.Entryは、getKeyでキーを、getValueで値を取得できます。ですので、このペアでMapに登録されているということですね。
// Java 11の日本語版Javadocより抜粋 Set<Map.Entry<K,V>> entrySet() 戻り値: マップ内に保持されているマッピングのセット・ビュー
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); map.put("ポップスの帝王", "マイケル"); Set<Map.Entry<String, String>> entries = map.entrySet(); System.out.println(entries.size()); // → 3、mapにはキーと値のペアが3つあるため System.out.println(entries); // → [ポップスの帝王=マイケル, 猫=マイケル, 犬=伸之助] for (Map.Entry<String, String> entry : entries) { System.out.println("キーは[" + entry.getKey() + "]、値は[" + entry.getValue() + "]です"); }
3.【応用】Mapの活用方法いろいろ
キーと値を使ってデータをMapの中に保存する…と言われても、具体的な活用方法が思い浮かばない方もいらっしゃるでしょう。プログラミングの初心者なら、むしろそれが普通です。
この章では、実際のJavaプログラミングでのMapの使用例をいくつかお伝えします。
3-1.データの集計・分類・加工に使う
Mapはデータをメモリ上で集計・分類・加工する時によく使います。キーを集計キーとして、値を集計結果にします。
SQLをご存じなら、GROUP BYをする列がキー、SUMやAVGした結果が値となるイメージです。もちろん、集計の方法は自由にプログラミングできますから、応用の方向性は色々です。
実用上では、キーにはStringがよく使われます。もちろん、キーとなり得るなら別のクラスでもOKです。値としては、集計結果は何かを足しこめるような値(数値のプリミティブ型のラッパークラスなど)やクラスにしておくといいでしょう。
集計処理では、Mapにキーがなければ新しくputし、あれば値へ何かの処理をして更新後の値でputをします。これはMapの使い方のイディオムでもあります。
List<String> data = Arrays.asList("A,100", "B,50", "C,200", "A,150", "B,1000"); // キー文字列、値を持ったデータ Map<String, Integer> groupBy = new HashMap<>(); // キーはString、値は数字を合計するのでIntegerとしている for (String d : data) { String[] items = d.split(","); Integer num = Integer.valueOf(items[1]); Integer sum = groupBy.get(items[0]); if (sum == null) { groupBy.put(items[0], num); } else { sum += num; groupBy.put(items[0], sum); } } System.out.println(groupBy.get("A")); // → 250
集計結果を合計するのではなく、単にグループ化したいなら、Listなどを値とすればよいでしょう。
なお、以下の例のように、値がクラスならgetでインスタンスへの参照が得られますので、あらためてputし直す必要はありません。ここもMapで気を付けたいところです。
List<String> data = Arrays.asList("A,100", "B,50", "C,200", "A,150", "B,1000"); // キー文字列、値を持ったデータ Map<String, List<Integer>> groupBy = new HashMap<>(); // キーはString、値は数字を集めるのでIntegerのListとした for (String d : data) { String[] items = d.split(","); Integer num = Integer.valueOf(items[1]); List<Integer> sum = groupBy.get(items[0]); if (sum == null) { sum = new ArrayList<>(); groupBy.put(items[0], sum); } sum.add(num); } System.out.println(groupBy.get("A")); // → [100, 150]
3-2.データ構造の簡易的な表現に使う
MapのキーになるStringの一覧をあらかじめ決めておけば、クラスの代わりに使える簡易的なデータ構造として使えます。クラスを作るほどでもない場合や、データを渡す先がネットワークの向こう側にあってクラスでやり取りできない場合などで使えるでしょう。場合によっては、Mapがネストします。
// Mapでユーザの情報を表現してみる Map<String, Object> userAttribute = new HashMap<>(); userAttribute.put("userId", 100); userAttribute.put("userName", "マイケル"); userAttribute.put("mailAddress", "michael@example.com"); userAttribute.put("birthday", LocalDate.of(2019, 1, 1)); userAttribute.put("trait", Arrays.asList("猫", "もふもふ")); // ユーザの情報を管理するMapを別に作り、数値のユーザIDをキーにしてMapを値として対応付ける Map<Integer, Map<String, Object>> users = new HashMap<>(); users.put(100, userAttribute); // ユーザ情報を持っているMapをこんな感じで使う System.out.println(users.get(100).get("userName")); // → "マイケル"
この場合に重要なのは、Mapにはどういうキーと値があるかという取り決めです。Mapの構造をJavadocやプログラムの仕様書などで明確にしておく必要があります。特に、Mapを汎用的な構造として使うなら、この例のように値がObjectになりがちなので、キーの値が何なのかは事前に明確にしておきます。
3-2-1.【参考】Mapのキーと値には用途に適したクラスが好ましい
ただ、こういう用途でも、キーや値に独自のクラスを使う方が、プログラムが分かりやすくなるケースが多いです。適切な変数名やメソッドが使えれば意味が分かりやすくなりますし、型が明確になるのでコンパイラレベルでチェックができるなど、メリットが多いものです。
それに、Mapを使う範囲がクラスの中に閉じているなら、必要に応じて内部クラスを積極的に作って利用するのが、Javaのプログラミングスタイルです。ただ、そういう内部クラスはクラスの外部から見えない・使えないようにしないと混乱を招きますので、注意しましょう。
3-3.データのキャッシュに使う
キーとなる情報は少数で明確、その一方で値は複雑で作るのに手間も時間もかかる…そんな場合は、Mapを値のキャッシュとして使えます。
よく使うのは、キーとしたい複数のStringを、何かのセパレータ(キー内で使われない文字が好ましい)で繋いだものを、Mapのキーとすることです。あるいは、キー情報そのものをクラスにしてしまえるなら、他でもいろいろと流用ができますので、より良いやり方です。
// キーが2つのStringで表現できる場合 private Map<String, VeryComplexClass> cache = new HashMap<>(); // キャッシュとして使うMapを作る。内容は初期状態では空。 public VeryComplexClass getVeryComplexClass(String key1, String key2) { String key = key1 + "\t" + key2; VeryComplexClass value = cache.get(key); // キャッシュの中に探している値があったので即returnする if (value != null) { return value; } // キャッシュの中に探している値がなかったので、生成してキャッシュにputする value = new VeryComplexClass(); // その他いろいろ複雑な初期化処理… cache.put(key, value); return value; }
// キーをクラスとした場合 private Map<VeryComplexClassKey, VeryComplexClass> cache = new HashMap<>(); // キャッシュとして使うMapを作る。内容は初期状態では空。 public VeryComplexClass getVeryComplexClass(VeryComplexClass key) { VeryComplexClass value = cache.get(key); // キャッシュの中に探している値があったので即returnする if (value != null) { return value; } // キャッシュの中に探している値がなかったので、生成してキャッシュにputする value = new VeryComplexClass(); // その他いろいろ複雑な初期化処理… cache.put(key, value); return value; }
キャッシュ用途ではSetも選択肢ですが、Mapとは使い道が違います。Setの機能は値を一意にすることなので、値を単純に溜めこんで、後からまとめて何かをする…という用途に向いています。Setは、StringやIntegerなどの単純な値ならともかく、複雑なデータの検索のしやすさではMapよりも不利なのです。
3-4.【参考】Mapをファイルへ読み書きする
Mapのキーや値をSerializableなクラスにしておけば、ObjectInputStream/ObjectOutputStreamを使って、Map(の実装クラス)のインスタンスをファイルやネットワーク経由で読み書きできます。Mapのキーと値をテキストファイルなどに書き出して再度読み込み、putし直すよりも簡単です。
これを使うケースはあまりないかもしれません。でも、できるということは頭の片隅で覚えておくと、便利に使える時があるかもしれませんよ?
// Mapを作って… Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); // ObjectOutputStreamでファイルへMapを出力して… try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("/A/B/animals.ser"))) { oos.writeObject(map); } // ObjectInputStreamでファイルからMapを読み込む!! try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("/A/B/animals.ser"))) { map = (Map<String, String>)ois.readObject(); } System.out.println(map.get("猫")); // → "マイケル"!! ファイルにMapの内容が保存できた!!
4.【重要】Mapを使うのに欠かせないもの
4-1.【必須】Object.equalsはキーが同じかの判断に使う
Mapのキーに使うクラスには、インスタンスが「同じ」か判断するメソッドObject.equalsが「正しく」オーバーライドされていなければなりません。これはMapを使う上では必須です。Mapからのgetでは、Mapが持つキーのインスタンスの中から、equalsで同じものに対応付けられた値を得るからです。
Mapのキーにはあなたが使いたいクラスを使えます。でも、自分で作ったクラスを使う場合は、equalsが「正しく」作られているか気を付けましょう。一方、Java標準APIのクラス(String等)はしっかりとequalsが作られていますので、普通は問題ありません。
Object.equalsの詳細は、以下の記事を参照してください。
関連記事4-2.Object.hashCodeは一部のMapの実装クラスでは必須
Object.hashCodeは、インターフェイスとしてのMapを使うには必須ではありません。でも、Mapの特定の実装クラス、例えばJavaの標準APIにあるHashMapやLinkedHashMapなどを使う時には、キーのクラスにhashCodeが「正しく」オーバーライドされていなければなりません。
実際のJavaプログラミングではHashMapが大変よく使われます。HashMapでキーとするクラスが自分で作ったものなら、hashCodeが「正しく」作られているか確認しましょう。そうしないと、プログラムが上手く動かない原因になります。
Object.hashCodeの詳細は、以下の記事を参照してください。
関連記事4-3.Comparableはぜひ意識したいインターフェイス
インスタンスの大小を比較するComparableは、Mapを使う上ではぜひ意識したいインターフェイスです。一部のMap実装クラスを使うなら必須ですし、性能向上につながることもあります。
例えば、Mapの実装クラスとしてTreeMapを使うには、キーがComparableであるか、あるいはComparatorを使った大小比較が出来なければなりません。TreeMapはキーの「順番」を意識してくれるMapですが、そのためにはキーの大小を比較するComparable.compareToか、Comparatorが必要なのです。
そして、キーがComparableを実装していてキーの大小比較ができると、一部のMapでは実装クラスの性能がよくなります。例えば、HashMapでキーの大小比較ができると、大量データを扱う場合のデータ管理をより効率的に行えるので、putする時などでレスポンスがよくなります。
Comparableの詳細は、以下の記事を参照してください。
関連記事5.【発展】MapにJava 8以降で追加された機能
MapはJava 8で多くのメソッドが追加され、以後も機能強化は続いています。いままで自分で書いていた処理を肩代わりしてくれるものも多いので、ぜひ活用しましょう。
ここでは、Java 8以降で追加されたメソッドの中で「これは!!」というものの使い方をお伝えします(Java 11時点)。なお、以下で詳細を紹介していないメソッドについて簡単にまとめると、以下となります。
getOrDefault:getする際に、キーの対応付けがない時のデフォルト値を指定できる
forEach:保持するキー・値の対応でループする。entrySet→ループを書かなくていいためお手軽。
replaceAll:値全体を処理結果で一括置換する
remove(オーバーロード版):キーと値が一致する場合のみ削除する
ofEntries:Mapが既に持っている、特定の複数エントリのみ持った読み取り専用のMapを簡単に作る
entry:Mapが既に持っている、特定のキーと値だけ持つ読み取り専用のMapを作る
なお、Java 8で追加された関数型インターフェイスを使うメソッドが多いので、活用したい場合は関数型インターフェイスやラムダ式を勉強しておくとよいでしょう。最初は正直言ってとっつきづらいですが、慣れてくるとそれらなしではいられなくなりますよ。
5-1.putIfAbsentで、対応付けがない時の値を指定する【Java 8~】
absentは「ない」という意味の単語です。メソッド名を素直に読むと「ないならputする」で、実際の動きもキーがない場合にだけputするものです。戻り値は、メソッドを呼び終わった時点でキーに対応付けられている値になります。
// Java 11の日本語版Javadocより抜粋 default V putIfAbsent(K key, V value) パラメータ: key - 指定された値が関連付けられるキー value - 指定された鍵に関連付けられる値 戻り値: 指定されたキーに関連付けられた以前の値。キーのマッピングがなかった場合はnull。
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); map.putIfAbsent("猫", "ニャジラ"); map.putIfAbsent("狸", "ぽこにゃん"); System.out.println(map.get("猫")); // → "マイケル"、既にキー"猫"があったので、putされなかった System.out.println(map.get("狸")); // → "ぽこにゃん"
putIfAbsentは、従来は以下のようにしていた処理の代わりになります。if文で判断しなくてもいいので、プログラムが短くなります。
String value = map.get("猫"); if (value == null) { value = map.put("猫", "ニャジラ"); } return value;
5-2.replaceでキーがあるなら値を入れ替える【Java 8~】
replaceは、引数のキーがMapにあれば値を入れ替え、キーがなければ何もしません。戻り値は入れ替えがされたかどうかのbooleanで、入れ替えされたらtrue、されなかったらfalseです。
// Java 11の日本語版Javadocより抜粋 default boolean replace(K key, V oldValue, V newValue) パラメータ: key - 指定された値が関連付けられるキー oldValue - 指定されたキーに関連付けられていると予想される値 newValue - 指定されたキーに関連付けられる値 戻り値: 値が置換された場合はtrue
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); map.replace("猫", "ニャジラ"); map.replace("狸", "ぽこにゃん"); System.out.println(map.get("猫")); // → "ニャジラ" System.out.println(map.get("狸")); // → null
replaceは、従来は以下のようにしていた箇所の代わりになります。
if (map.containsKey("猫")) { return map.put("猫", "ニャジラ"); } else { return null; }
5-3.compute/mergeで値の加工をする【Java 8~】
Mapのキーに対応付けられた値を加工・編集して入れ替えたいことは良くあります。Mapの活用例で挙げたデータの集計はその典型例です。その用途には、Java 8以降ならcomputeや関連するメソッドを使ってみましょう。if文のブロックが少なくなって、スッキリしますよ。
// Java 11の日本語版Javadocより抜粋 default V compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction) パラメータ: key - 指定された値が関連付けられるキー remappingFunction - 値をコンピュートするための再マップ関数 戻り値: 指定されたキーに関連付けられる新しい値。存在しない場合はnull
List<String> data = Arrays.asList("A,100", "B,50", "C,200", "A,150", "B,1000"); // キー文字列、値を持ったデータ Map<String, Integer> groupBy = new HashMap<>(); // キーはString、値は数字の集計値 for (String d : data) { String[] items = d.split(","); Integer num = Integer.valueOf(items[1]); groupBy.compute(items[0], (k, v) -> v == null ? num : v + num); } System.out.println(groupBy.get("A")); // → 250
computeの引数は、キーと、値を編集するBiFunctionのインスタンスです。BiFunction.applyへの引数はキーとその時点での値が渡され、戻り値はキーに対応付けられる更新後の値です。この例では、Mapにキーがなければ数値を新たに対応付けて、あれば既存の値に新しい数値を足しこんでいます。
なお、computeIfAbsent/computeIfPresent/mergeも似たような処理を行います(mergeのみJava 10~)。違うのは、キーがある・ないなどの状況に合わせた、BiFunctionの呼ばれ方です。大まかにまとめると以下のとおりですが、詳細はJavadocを参照してください。
computeIfAbsent:Mapにキーがない、あるいはキーはあるが値がnullの場合に、BiFunction.applyが実行される
computeIfPresent:Mapにキーがあり値がnullでない場合に、BiFunction.applyが実行される
merge:computeIfAbsentと同じ動き。ただし、BiFunction.applyの戻り値がnullならキーがremoveされる
5-4.of/copyOfで変更できないMapを簡単に作る
5-4-1.ofで変更できないMapを作る【Java 9~】
Mapを使う時は、普通はMapの実装クラスをnewしてputをします。ですが、Mapが変更できなくてもよければ、Map.ofを使って簡単にMapのインスタンスを作れます。このMapへputやremove、clearなどのMapの内容を変えるメソッドを呼び出すと、UnsupportedOperationExceptionがthrowされます。
// Java 11の日本語版Javadocより抜粋 static <K,V> Map<K,V> of(K k1, V v1) パラメータ: k1 - マッピング・キー v1 - マッピング値 戻り値: 指定されたマッピングを含むMap
Map<String, String> map = Map.of("猫", "マイケル", "犬", "伸之助"); map.put("狸", "ぽこにゃん"); // コンパイルエラーにはならないが、実行するとUnsupportedOperationExceptionがthrowされる
この例では、2つのキーと値を対応させたMapを作りました。ofには、0~10個までのキーと値を一度に対応させるためのオーバーロードされたメソッドがありますので、必要に応じて使いましょう。
Map<String, String> map = Map.of( "K1", "V1", "K2", "V2", "K3", "V3", "K4", "V4", "K5", "V5", "K6", "V6", "K7", "V7", "K8", "V8", "K9", "V9", "K10", "V10");
5-4-2.copyOfでMapのスナップショットを作る【Java 10~】
Map.copyOfを使うと、引数のMapが読み取り専用(不変)になったMapが戻ります。ofと同様に、copyOfで作ったMapに変更操作を行おうとすると、UnsupportedOperationExceptionがthrowされます。
// Java 11の日本語版Javadocより抜粋 static <K,V> Map<K,V> copyOf(Map<? extends K,? extends V> map) パラメータ: map - エントリが描画されるMapはnullでなくてはなりません 戻り値: 与えられたMapのエントリを含むMap
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); // Map.copyOfでこの時点でのスナップショットを作る Map<String, String> map2 = Map.copyOf(map); // そのあとに、元のMapの値を変更+キーを追加する map.put("猫", "ニャジラ"); map.put("ポップスの帝王", "マイケル"); System.out.println(map2.get("猫")); // → "マイケル"、copyOfの時の値のまま System.out.println(map2.get("ポップスの帝王")); // → null、copyOfした時には"ポップスの帝王"はMapにキーとしてないため map2.put("狸", "ぽこにゃん"); // コンパイルエラーにはならないが、実行するとUnsupportedOperationExceptionがthrowされる
なお、不変のMapを作るには、Collections.unmodifiableMapもあります。違いは、不変のMapの元になったMapの変更が反映されるかです。元のMapへの変更が、Collections.unmodifiableMapは反映され、Map.copyOfは反映されません。ですから、Map.copyOfはMapのスナップショットを作っているとも言えます。
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); // Collections.unmodifiableMapで不変なMapを作る Map<String, String> map2 = Collections.unmodifiableMap(map); // そのあとに、元のMapの値を変更+キーを追加する map.put("猫", "ニャジラ"); map.put("ポップスの帝王", "マイケル"); System.out.println(map2.get("猫")); // → "ニャジラ" System.out.println(map2.get("ポップスの帝王")); // → "マイケル"、unmodifiableMap後でもMapの内容は連動する map2.put("狸", "ぽこにゃん"); // コンパイルエラーにはならないが、実行するとUnsupportedOperationExceptionがthrowされる
6.【発展】Mapにまつわるあれこれ
6-1.Mapの実装クラスの特徴(Java標準API)
プログラミングで良く使う、Java標準APIのMap実装クラスの特徴を一覧にしてみます。良く使うのはHashMapとTreeMapです。その他のMapや、ここには記載しなかったMapにも、それぞれ使いどころがあります。
クラス名 | Mapの特徴 | キーが持つべき特性 | キー/値にnullが可能か |
HashMap | 最もスタンダードなMap。hashCodeによるキーの管理が行われる。 | equals + hashCode | nullキー=可 null値=可 |
LinkedHashMap | キーの追加順番を覚えているMap。例えばkeySetでforEachすると、キーの追加順番で取り出せる。覚える順番は、コンストラクタでの指定次第でLRU(Least Recently Used)にもできる。 | equals + hashCode | nullキー=可 null値=可 |
TreeMap | キーをソートして管理するMap。例えばkeySetで取得したSetでforEachすると、ソート済みの結果でキーが取り出せる。キーがComparableであるか、コンストラクタでComparatorを与える必要あり。 | equals + Comparable ※TreeMapにComparatorを与えれば、キーはComparableでなくてもいい | nullキー=不可 (※キーがComparableの場合。Comparatorの実装方法によりnullでも可となる) null値=可 |
EnumMap | キーとしてEnumのみ使える特殊なMap。動作はとても高速。 | Enumのみキーに使える | nullキー=不可 null値=可 |
WeakHashMap | 弱参照でキーを持つMap。ガベージコレクション時に、Mapの外で強参照されていないキーが自動でremoveされる。OutOfMemoryError対策やキャッシュ用として有効。 | equals + hashCode | nullキー=可 null値=可 |
ConcurrentHashMap | 並列実行に対応したHashMap。複数スレッドからの同時アクセスがあっても、保持内容の整合性が保たれる。 | equals + hashCode | nullキー=不可 null値=不可 |
6-2.Mapのキーと値を型引数で絞り込んで、分かりやすくする
前述したとおり、上手なMapの使い方は、Mapがキーや値として扱えるクラスを型引数を使って絞り込み、Mapへプログラマが指定したキー・値以外のクラスをputできなくすることです。
キーを制限しているいい例がEnumMapです。ObjectやStringではキーとして使える「範囲」が広すぎるので、Enumの値をキーにすることで、キーとして使えるものを明確に制限できていますし、用途も明確です。また、Enumは存在する値が固定なので、処理の高速さにも繋がっています。
Java 1.4以前はJavaには型引数の仕組みがなく、Mapはキーも値もメソッド宣言上はObjectでした。しかも悪いことに、Mapでどんなクラスがキーや値に使われているかプログラマが確実に知る方法がありませんでした。Javadocやプログラム中のコメントに書くのがせいぜいという、今では信じられない状態です。
ですから、値をgetする時はキャストが必要で、しかもMapに何が入っているか分からないので、ClassCastExceptionがよく発生しました。それを防ぐためのinstanceofがあったりなど、プログラムも冗長でした。そして、プログラマがその頃の古い知識を更新できていないと、今でもObjectを使いがちです。
6-3.Mapが存在する限り、キーと値はガベージコレクションされない
Mapのインスタンスがメモリ上にあり続けるなら、Mapが持っているキーや値のインスタンスもずっとメモリ上に残り続けます。Mapが持っているので、それらのインスタンスが使われているとガベージコレクション時に判断されるからです。
もし、集計処理などでMapを一時的に使うだけならローカル変数としたり、フィールドなどの比較的寿命が長い変数とするなら適切なタイミングでremove/clearしましょう。そうしないと、いわゆるメモリリークに繋がってしまいます。
また、WeakHashMapなどの「弱参照」をサポートするMapを使ってもよいでしょう。WeakHashMapは、キーがWeakHashMap外でJavaのどこからも使われなくなったなら、自動的にキーを削除(=結果的に値も削除される)してくれる便利なものです。
6-4.keySet/values/entrySetは元のMapと連動する
keySet/values/entrySetで取得したSetやCollectionへの操作は、元のMapと連動します。removeやclearは特に影響が大きいです。プログラムのバグを防ぎたかったり、プログラムの中で予期せぬ動きをさせたくないなら、情報の読み取りだけにした方が無難でしょう。
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); Set<String> keys = map.keySet(); System.out.println(keys); // → [猫, 犬] keys.remove("猫"); // keySetで取得したSetから"猫"を削除すると… System.out.println(map.containsKey("猫")); // → false!! 元のMapのキーからも"猫"が削除された
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); Collection<String> values = map.values(); System.out.println(values); // → [マイケル, 伸之助] values.remove("マイケル"); // valuesで取得したCollectionから"マイケル"を削除すると… System.out.println(map.containsKey("猫")); // → false!! 元のMapからも"マイケル"と紐付けたキーが削除された
Map<String, String> map = new HashMap<>(); map.put("猫", "マイケル"); map.put("犬", "伸之助"); Set<Map.Entry<String, String>> entries = map.entrySet(); System.out.println(entries); // → [猫=マイケル, 犬=伸之助] entries.clear(); // → entrySetで取得したSetをclearする System.out.println(map.isEmpty()); // → true!! 元のMapからも全キー・値の対応が削除された
6-5.変更できるMapはクラスの外部に公開しない
内容を変更できるMapは、クラスの外部に直接公開しないようにしましょう。Mapそのものをそのまま公開してしまうと、クラス自身が責任を持って管理すべきデータを、クラスの外から変更出来てしまうことになるからです。つまり、データのカプセル化を維持できなくなります。
class PublicMap { // Mapをクラスの中だけで使いたいと思っていても… private Map<String, Integer> map = new HashMap<>(); // Mapのインスタンスをそのままreturnすると、呼び出し元でput/remove/clearができてしまう!! Map<String, Integer> getMap() { return map; } }
どうしてもMapを公開しなければならないなら、不変な(=変更できない)Mapを作るCollections.unmodifiableMap、あるいはMap.copyOfなどを使いましょう。Collections.unmodifiableMapなら、元になったMapと内容が連動します。Map.copyOfは、元のMapのスナップショットであり、内容は連動しません。
class PublicMap { private Map<String, Integer> map = new HashMap<>(); // 不変なMapにすれば、呼び出し元ではput、remove、clearなどはできなくなる Map<String, Integer> getMap() { return Collections.unmodifiableMap(map); } }
でも、不変なMapであっても、Mapのキーや値が持つメソッドそのものは呼べてしまいます。Mapが持つキーと値も不変なインスタンスにして外部に公開できるならベストですが、さすがにそこまでするとプログラミングが大変ですし、少々現実的ではありません。
ですから、Mapそのものをクラスの外部に公開する必要があるかは、よくよく考えましょう。Mapを管理しているクラスに専用のメソッドを用意して、データのチェックや値の取得をさせるだけでも十分なケースは多いと思いますよ。
6-6.クラス外部とのIFでは、素のMapをなるべく使わない
前節とも関連しますが、Mapが使われるJavaのプログラムに「うーん…」と感じる時は、素のMapがメソッドの引数や戻り値である時です。しかも、そんなMapは、えてしてキーも値もObjectです。そういうMapは、何にでも使える便利さと引き換えに、プログラムで本来表現すべき「文脈」が貧弱になっています。
class PlainMap { // キー・値でObjectを扱うMapがメソッドの引数 void method1(Map<Object, Object> param) { // 何かの処理 } // キー・値でObjectを扱うMapがメソッドの戻り値 Map<Object, Object> method2() { // 何かの処理 } }
フレームワークがMapを使う際は汎用的に作らざるを得ないので、データのやり取りにMapを使い、かつキーも値もObjectになるのは仕方がありません。でも、自分で作るプログラムなら、やりとりするデータを的確に表現したクラスをメソッドは受け取るべきですし、戻すべきです。
あなたのクラスのメソッドは、果たして本当にMapを受け取り、戻さなければならないのかを自問しましょう。Mapを使うのは安易な逃げ道としてではありませんか? Mapを簡易的なデータ構造として使いたいのなら、それをクラス化した方がいいかもしれません。あるべき姿をしっかりと考えてみましょう。
6-7.変数の型をMapと実装クラスのどちらで宣言すべきか
これはなかなか難しいです。原則はインターフェイスのMapとすべきと考えています。実装クラスが何であれ、Mapとして抽象的に扱えることが、インターフェイスとしてのMapが持つ利点であったり、存在意義だと考えているからです。
// 変数の型は、MapとMapの実装クラスのどちらであるべき?① Map<String, Integer> map1 = new HashMap<>(); HashMap<String, Integer> map2 = new HashMap<>();
// 変数の型は、MapとMapの実装クラスのどちらであるべき?② class TypeOfMap { void method1(Map<String, Integer> map) { // 何かの処理 } void method2(HashMap<String, Integer> map) { // 何かの処理 } }
でも、実装クラスであるべきかも…と考える時もあります。なぜなら、Mapの実装クラスにより、実際にサポートしているメソッドや、メソッドの実際の振る舞いが実は違うからです。例えば、TreeMapはnullのキーと値を使えませんが、HashMapはキーも値もnullを使えます。
そして、MapのAPIには「オプションの操作(英語版だとoptional operation)」の記述があります。つまり、それらは実装クラスで完全に動作しなくてもいいのです。Mapのインターフェイスにあるメソッドなのでどの具象クラスでも呼べますが、呼ぶ側が意図する振る舞いをすることは、必ずしも保証されません。
そういう振る舞いの違いは、Mapの実装クラスのJavadocには明確に書かれています。ですが、インターフェイスのMapで扱うと、その辺りの事情が分からなくなります。結局、明確な答えはないのですが、Mapを使っていて何が問題が起きた時は、実装クラスが何かを意識してみましょう。
7.まとめ
Mapはキーと値をセットにして扱うためのデータ構造です。キーという中間的なものを間に挟むことで、高速・効率的なデータ管理が行えます。
Mapの操作は、キーと値を設定する、キーに紐づく値を得る、キーを削除する、キー・値を取得するが基本的なものです。それらを組み合わせて、データの集計・分類・キャッシュなどに用いるのが、Mapのよくある使い方です。
Mapはキーや値に任意のクラスを使えます。ですが、扱える範囲が広すぎると逆に収拾がつかなくなり、ゴミ箱のようになりがちです。型引数などを使ってMapで管理する対象を明確にするのが、上手なMapの使い方です。
そして、便利だからと言っていたずらにクラスの内部で持つMapを外部に公開することは、データのカプセル化を破ることにも繋がりかねない危険な行為です。オブジェクト指向の利点を生かすためにも、Mapは適材適所で用いましょう!!
コメント