マップ
- 解説
- 1. マップとは
- 2. マップの宣言
- 3. マップと要素数・ループ
- 4. マップの操作
解説
1. マップとは
配列やリストに使用してきた添字は、先頭から何番目の要素であるかを示すただの番号でした。要素を特定する際に文字列を使用できる仕組みがマップです。
一般的に、配列やリストでは添字をIndexと表現しますが、マップではKeyと表現します。また、この「KeyとValue(要素の値)」を紐づけて管理する配列を「連想配列」と呼びます。
配列のページで見たケーキ販売数カウント処理の例では、バラバラの変数を配列化したことで処理が簡潔に書けるようになりましたね。しかし、ケーキを特定する情報が番号になってしまったことで、どのケーキが何番なのかという情報を別途管理する必要ができてしまいました。
マップを利用すれば、ケーキの名前をそのままキーとして使用できるため、処理の簡潔さとデータの扱いやすさを両立できるのです。
// KeyがString型でValueがInteger型の連想配列でカウンターを作成
Map<String, Integer> sellCounter = new HashMap<>();
// 売れたケーキの情報をレジなどから随時受け取る何らかの処理
// cakeNameがケーキの名前、sellCountが売れた数とする
~~~
// 情報を受け取る度に販売数を加算する処理
// getOrDefaultで今の販売数を取得して、加算した値を連想配列にput
// 販売数が1回もputされていなければ、getOrDefaultの結果は0になる
sellCounter.put(cakeName, sellCounter.getOrDefault(cakeName, 0) + sellCount);
次のサンプルコードの動きを、リストのページのサンプルと見比べてみましょう。
static void practiceMap() {
Map<String, String> stringMap = new HashMap<String, String>(); // 宣言
stringMap.put("dog", "いぬ"); // 要素追加
stringMap.put("cat", "ねこ"); // 要素追加
Logger.out(stringMap.get("cat")); // 参照
stringMap.put("cat", "猫"); // 要素内容変更
Logger.out(stringMap.get("cat")); // 参照
stringMap.put("chicken", "にわとり"); // 要素追加
Logger.out(stringMap.get("chicken")); // 参照
stringMap.remove("dog"); // 要素削除
Logger.out(stringMap.get("dog")); // 削除済みでもエラーではなくnull
Logger.out(stringMap.get("cow")); // 元々存在しなくてもnull
}
[2020/12/31 12:34:56.789] ねこ
[2020/12/31 12:34:56.789] 猫
[2020/12/31 12:34:56.789] にわとり
[2020/12/31 12:34:56.789] null
[2020/12/31 12:34:56.789] null
2. マップの宣言
マップの宣言は以下のように行います。形としてはリストとだいたい同じですね。
まず「Map」というのは、「配列のように同じ変数(要素)をたくさん持てること」や、「要素を追加・削除できること」、「拡張for文で列挙できること」、さらに「文字列(など)を添字に使用できること」などの「ルール」を持った「インターフェース」です。インターフェースはルールだけで中身がありません。
右辺の「HashMap」というのは、Mapインターフェースのルールをきちんと守っている、中身を持ったクラスです。つまり上図では、Mapインターフェースのルールを守ったクラスの実体を入れる変数stringMapを宣言し、そこにHashMapクラスの実体を代入している、ということになります。
Listとの違いは、「<>」内の1つ目で添字(キー)がString型であることを明示していることです。2つ目はListと同様に、各要素がString型であることを明示しています。これによって、この変数の各要素をString型として扱うことが可能となり、他の型を代入しようとするとエラーとなります。右辺の「<>」の中身は省略することが可能です。
List同様、「<>」の中には基本型を指定できません。そのため、数値ならばInteger型やLong型、論理値ならばBoolean型、というようにラッパークラスで型を指定してください。
なお、キーにはString型以外も指定できます。例えばInteger型で連続しない数値をキーにするなどが考えられますが、その使い方が本当に効果的で安全か、よく考えて使用しましょう。
3. マップと要素数・ループ
マップは配列やリストと異なり、添字が0から始まり要素数 - 1までという特徴を持っていないため、ループによる列挙は工夫が必要となります。
先にキーだけを配列で取得し、それを列挙して対応する値を取得する方法と、Entryというキーと値をセットで扱える型を使用する方法があります。いずれもまだ学習していない仕組みが多用されていますので、使い方だけ見ておきましょう。
static void practiceMapLoop() {
Map<String, String> map = new HashMap<String, String>();
map.put("いぬ", "わん!");
map.put("ねこ", "にゃん!");
map.put("にわとり", "こけこっこー!");
Object[] keys = map.keySet().toArray(); // キーだけを配列で取得
for (Object key : keys) { // キーだけを列挙
String stringKey = (String) key; // 本来のキーの型に変換
Logger.out(stringKey + ":" + map.get(stringKey));
}
// Entryというキーと値がセットの型に代入しながら列挙
for (Map.Entry<String, String> entry : map.entrySet()) {
Logger.out(entry.getKey() + ":" + entry.getValue());
}
}
4. マップの操作
配列では添字を使って各要素を指定して代入や参照を行いましたね。マップはHashMapというクラスが持っているメソッドなどを使用して次のような操作が行えます。
- 追加・変更:putメソッド
- 取得:getメソッド
- 削除:removeメソッド
- 全削除:clearメソッド
- 並び替え:keySetメソッド
- キーが含まれているか:containsKeyメソッド
- キーの有無による取得の振分:getOrDefaultメソッド
追加・変更:putメソッド
putメソッドでは、丸括弧に入れる値(以下、引数と呼びます)の1つ目にキーを、2つ目に値を指定することで、そのキーと値の組み合わせを追加します。
なお、同じキーを指定した場合には元の値を上書きします。
static void practiceMapPut() {
Map<String, Integer> map = new HashMap<>();
map.put("1月", 31); // キー1月と値31の組み合わせを追加
map.put("2月", 28); // キー2月と値28の組み合わせを追加
map.put("3月", 31); // キー3月と値31の組み合わせを追加
map.put("4月", 30); // キー4月と値30の組み合わせを追加
map.put("2月", 29); // キー2月の値を29に変更
// Mapの場合は追加した順序と列挙される順序に関係性なし
for (Map.Entry<String, Integer> entry : map.entrySet()) {
Logger.out(entry.getKey() + ":" + entry.getValue());
}
}
[2020/12/31 12:34:56.789] 4月:30 // 列挙の順序はバラバラ
[2020/12/31 12:34:56.789] 3月:31
[2020/12/31 12:34:56.789] 2月:29 // 2回目のputで値が上書きされている
[2020/12/31 12:34:56.789] 1月:31
取得:getメソッド
getメソッドでは、引数でキーを指定した要素の値を取得します。存在しないキーを指定するとnullが取得されます。
static void practiceMapGet() {
Map<String, String> map = new HashMap<>();
map.put("春", "spring");
map.put("夏", "summer");
map.put("秋", "autumn");
map.put("冬", "winter");
Logger.out(map.get("秋")); // 「秋」の要素の値
String season = "冬"; // キーを変数で指定
Logger.out(map.get(season)); // 「冬」の要素の値
season = "梅雨"; // 存在しないキー
Logger.out(map.get(season)); // 対応するキー無しの場合はnull
}
[2020/12/31 12:34:56.789] autumn
[2020/12/31 12:34:56.789] winter
[2020/12/31 12:34:56.789] null // エラーとならずnullになる
削除:removeメソッド
removeメソッドでは、引数でキーを指定した要素を削除します。
static void practiceMapRemove() {
Map<String, String> map = new HashMap<>();
map.put("dog", "いぬ");
map.put("cat", "ねこ");
map.remove("dog"); // 削除
Logger.out(map.get("dog"));
Logger.out(map.get("cat"));
}
[2020/12/31 12:34:56.789] null
[2020/12/31 12:34:56.789] ねこ
全削除:clearメソッド
clearメソッドでは、要素をすべて削除します。
static void practiceMapClear() {
Map<Integer, String> map = new HashMap<>();
map.put(100, "百");
map.put(1000, "千");
map.put(10000, "万");
map.clear();
for (Map.Entry<Integer, String> entry : map.entrySet()) {
Logger.out(entry.getKey() + ":" + entry.getValue());
}
Logger.out("要素が無いため出力も無い");
}
[2020/12/31 12:34:56.789] 要素が無いため出力も無い
並び替え:keySetメソッド
Map(HashMap)では、追加した順序で要素が管理されないため、ループ処理すると思いがけない順番で列挙されます。Mapの中身自体を並び替えることはできないため、キーだけを並び替えてその順番でgetします。
キーだけを取得する処理は、keySetメソッドで実現できます。並び替えはArraysクラスのsortメソッドを使用して行うため、keySetメソッドの結果をtoArrayメソッドで配列にしています。この際、取得できる配列はObject[]型となるため、Integer型への変換を行っています。
static void practiceMapSort() {
Map<Integer, String> map = new HashMap<>();
map.put(1, "一");
map.put(10, "十");
map.put(100, "百");
map.put(1000, "千");
map.put(10000, "万");
// Mapの場合は追加した順序と列挙される順序に関係性なし
for (Map.Entry<Integer, String> entry : map.entrySet()) {
Logger.out(entry.getKey() + ":" + entry.getValue());
}
Object[] keys = map.keySet().toArray(); // キーだけを配列で取得
Arrays.sort(keys); // キーだけを昇順で並び替え
for (Object key : keys) { // キーだけを列挙
Integer integerKey = (Integer) key; // Integer型に変換
Logger.out(key.toString() + ":" + map.get(integerKey));
}
}
[2020/12/31 12:34:56.789] 10000:万 // 順番はバラバラ
[2020/12/31 12:34:56.789] 1:一 // 順番はバラバラ
[2020/12/31 12:34:56.789] 100:百 // 順番はバラバラ
[2020/12/31 12:34:56.789] 1000:千 // 順番はバラバラ
[2020/12/31 12:34:56.789] 10:十 // 順番はバラバラ
[2020/12/31 12:34:56.789] 1:一
[2020/12/31 12:34:56.789] 10:十
[2020/12/31 12:34:56.789] 100:百
[2020/12/31 12:34:56.789] 1000:千
[2020/12/31 12:34:56.789] 10000:万
含まれているか:containsKeyメソッド
containsKeyメソッドでは、キーをequalsメソッドで先頭から比較して、一致するキーが見つかった場合はtrueを、見つからなかった場合はfalseを返します。
キーが存在する場合としない場合で処理を分ける際に便利です。
static void practiceMapContainsKey() {
Map<String, Integer> map = new HashMap<>();
map.put("ショート", 0);
map.put("ショート", map.get("ショート") + 20);
map.put("チーズ", 0);
map.put("チーズ", map.get("チーズ") + 30);
if (!map.containsKey("チョコ")) {
map.put("チョコ", 40); // キーがなければ
} else {
map.put("チョコ", map.get("チョコ") + 50); // キーがあれば
}
for (Map.Entry<String, Integer> entry : map.entrySet()) {
Logger.out(entry.getKey() + ":" + entry.getValue());
}
}
[2020/12/31 12:34:56.789] チョコ:40
[2020/12/31 12:34:56.789] チーズ:30
[2020/12/31 12:34:56.789] ショート:20
キーの有無による取得の振分:getOrDefaultメソッド
getメソッドでは、キーが存在しない場合にはnullという特殊な値が取得されました。「取得した値がnullの場合」や、「containsKeyでfalseの場合」など、キーが存在しない場合の処理は意外と面倒です。
getOrDefaultメソッドでは、引数の1つ目に指定したキーが存在しない場合には、2つ目に指定した値を取得したことにできます。これにより、if文などで処理を振り分けなくとも、「前の値」を利用した処理が簡潔になります。
static void practiceMapGetOrDefault() {
Map<String, Integer> map = new HashMap<>();
map.put("ショート", 0);
map.put("ショート", map.get("ショート") + 20);
map.put("チーズ", 0);
map.put("チーズ", map.get("チーズ") + 30);
// 0をputする処理が無くても、null + 50ではなく0 + 50にできる
map.put("チョコ", map.getOrDefault("チョコ", 0) + 50);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
Logger.out(entry.getKey() + ":" + entry.getValue());
}
}
[2020/12/31 12:34:56.789] チョコ:50
[2020/12/31 12:34:56.789] チーズ:30
[2020/12/31 12:34:56.789] ショート:20