JavaのMapとは?キーと値を紐付けるデータ構造の特徴と使い方
今回は、そのようなデータ構造の一つであるMap(マップ)を取り上げます。特徴や使い方についてサンプルコード付きで説明していくので、ぜひ活用してください。
今回は、そのようなデータ構造の一つであるMap(マップ)を取り上げます。特徴や使い方についてサンプルコード付きで説明していくので、ぜひ活用してください。
スキルアップ
2022/12/19 UP
- プログラミング
- Java
- システムエンジニア
どのようなデータ構造を使うべきかを目的に応じて考えることは、プログラミングをする際のポイントとなる要素です。Javaでは、そのために使える便利なデータ構造が多数提供されています。使い方を覚えれば、プログラミングの幅も広がるでしょう。
今回は、そのようなデータ構造の一つであるMap(マップ)を取り上げます。特徴や使い方についてサンプルコード付きで説明していくので、ぜひ活用してください。
Mapとは
Mapとは、「キー」を「値」に紐付けるデータ構造のことです。「キーを値にマッピングする」という意味合いで、「マップ」と呼ばれています。
Java以外にも、同様のデータ構造を利用可能なプログラミング言語は少なくありません。また、Mapにはいくつかの別名があり、「連想配列」や「辞書」などと呼ばれることもあります。
プログラミング経験者のなかには、「Mapは配列のようなものだ」というイメージをもっている人もいるでしょう。たしかに、どちらも複数のデータを格納しておけるデータ構造だという点については共通です。加えて、インデックス(添字)で要素にアクセスできる配列と、キーで値にアクセスできるMapは使い方も似たところがあります。
ただし、配列ではインデックスに整数値しか指定できません。これに対し、Mapには整数以外の型もキーとして使用できるなどの特徴があります。
配列のついて詳しく知りたい場合は、こちらの記事もチェックしてみてください。
Javaの配列とは?Arrayの使い方とサンプルを紹介
JavaにおけるMapの特徴
ここからは、JavaのMapについて詳しく説明していきます。まずは、その特徴についてみていきましょう。
キーで値を読み書きできる
JavaにおけるMapの操作では、キーを指定して値を読み書きするのが基本です。そのために、次の2つのメソッドが提供されています。
・get(key):キーに紐付られた値を取り出す
・put(key, value):キーに紐付けて値を格納する
Mapはキーによって値を識別するため、1つのMap内に同じキーは存在できません。これは、キーが決まれば、それに対する値も一意に決まるということです。また、既存のキーを指定して値を格納した場合は、もとの値が上書きされます。
3つのコレクション・ビューをもつ
JavaのMapには、3つの「コレクション・ビュー」があります。これは、Mapにはコレクション(データの集まり)としての、3種類の見方があるということです。
具体的には、以下の3つのメソッドでコレクションにアクセスできます。
・keySet():すべてのキーでできたSet型のコレクション
・values():すべての値でできたCollection型のコレクション
・entrySet():すべてのマッピングでできたSet型のコレクション
Set型とCollection型は、いずれもコレクションを表すデータ構造です。ただし、Set型ではデータの重複が許されていません。キーのコレクションがSet型であることからも、Map内には同じキーが存在しないとわかるでしょう。
また、値のコレクションはCollection型のため、1つのMapに同じ値を複数格納するのは問題ないこともわかります。Mapではキーさえ異なっていれば、それぞれの値を識別できるためです。
JavaにおけるMapの使い方
JavaによるプログラミングでMapを使用するための、具体的な方法についてみていきましょう。
Javaでは、Mapは「インターフェース(interface)」として定義されています。そのため、プログラム中ではMapを直接使うのではなく、Mapを実装した「クラス(class)」を用いなければなりません。
HashMap(ハッシュマップ)は、Mapの実装のなかでも代表的なクラスです。ここからはHashMapを中心に、Mapの使い方を説明していきます。
Mapを作成する
JavaのMapは、キーと値それぞれの型を指定して作成します。ここではキーを整数、値を文字列とするMapを作成してみましょう。それには、次のように記述します。
var myMap = new HashMap<Integer, String>();
Integerでキーを整数型、Stringで値を文字列型として、HashMapを作成している様子がわかるでしょうか。これを変数myMapに代入しているため、このあとはmyMapをMapとして使用できます。
キーが整数のため、このMapの使い方は配列に近いイメージです。次のようにキーと値を一緒に指定すれば、値を格納できます。
myMap.put(0, "りんご");
これにより、指定したキー(ここでは「0」)から値(ここでは「"りんご"」)へのマッピングがMap内に設定されます。
格納した値は、次のようにキーを指定すれば取り出すことが可能です。
var value = myMap.get(0); // "りんご"
この例では先ほどのキー(「0」)を指定しているので、対応する値(「"りんご"」)が取り出されて、変数valueに代入されます。
現在Map内にあるマッピングの数(キーと値のペアが全部で何組あるか)は、size()メソッドで取得可能です。
var numberOfMappings = myMap.size();
ここまでが、Mapを作成して値を読み書きするために必要な基本の操作です。
では、上記を1つのプログラムに組み合わせてみましょう。以下は、実際に実行可能なサンプルコードです。
サンプルコード:
import java.util.HashMap; public class Main { public static void main(String[] args) { // Mapを作成 var myMap = new HashMap<Integer, String>(); // Mapに値を格納 myMap.put(0, "りんご"); myMap.put(123, "ぶどう"); myMap.put(-99, "みかん"); // マッピングの数を表示 System.out.println("マッピングの数:" + myMap.size()); // Mapの値を取り出して表示 System.out.println("キー;0 / 値:" + myMap.get(0)); System.out.println("キー;123 / 値:" + myMap.get(123)); System.out.println("キー;-99 / 値:" + myMap.get(-99)); } }
実行結果:
マッピングの数:3 キー;0 / 値:りんご キー;123 / 値:ぶどう キー;-99 / 値:みかん
キーを整数とするMapを取り上げましたが、サンプルコード内ではキーを0から順番に使用しているわけではないことがわかるでしょうか。このようにキーが不連続だったり負数だったりしても問題なく操作できる点は、配列にはないMapの特徴といえます。
Mapのキーを文字列にする
キーの型が文字列のMapは、実際の開発でも用いられることの多いデータ構造です。ここでは、キーと値を両方とも文字列とするMapを作成してみましょう。それには、次のように記述します。
var myMap = new HashMap<String, String>();
指定する型が違う点を除けば、Mapの作り方はキーが整数の場合と変わらないことがわかるでしょう。
このMapに値を格納するには、次のようにします。
myMap.put("apple", "りんご");
ここではキーが「"apple"」、値が「"りんご"」です。値を格納する方法も、キーが文字列になった点以外はキーが整数の場合と変わらないことがわかります。
値を取り出す方法についても同様です。
var value = myMap.get("apple"); // "りんご"
これで指定したキー(「"apple"」)に対応する値(「"りんご"」)を取り出し、変数valueに代入できます。
以下は、上記を組み合わせたサンプルコードと、その実行結果です。
サンプルコード:
import java.util.HashMap; public class Main { public static void main(String[] args) { // Mapを作成 var myMap = new HashMap<String, String>(); // Mapに値を格納 myMap.put("apple", "りんご"); myMap.put("grape", "ぶどう"); myMap.put("orange", "みかん"); // マッピングの数を表示 System.out.println("マッピングの数:" + myMap.size()); // Mapの値を取り出して表示 System.out.println("キー;apple / 値:" + myMap.get("apple")); System.out.println("キー;grape / 値:" + myMap.get("grape")); System.out.println("キー;orange / 値:" + myMap.get("orange")); } }
実行結果:
マッピングの数:3 キー;apple / 値:りんご キー;grape / 値:ぶどう キー;orange / 値:みかん
あらかじめ格納しておいたデータ(=値)を、その名前(=キー)を手がかりに取り出している様子がわかるのではないでしょうか。このように、Mapのキーを文字列とするのは、データに名前をつけるようなものです。「辞書」という呼び方がふさわしいデータ構造だといえるでしょう。
Mapの内容をループで取得する
Mapに格納した値はキーを指定すればいつでも取り出せますが、そのためにすべてのキーを覚えておかなければならないというのでは困ることもあるでしょう。そこでMapには、すべてのキーと値を取得するための手段も用意されています。コレクション・ビューにアクセスし、ループによってデータを取り出す方法です。
3つあるコレクション・ビューのなかで、最初に理解しやすいのはkeySet()でしょう。次のようなループでキーを1つずつ取得でき、それぞれに紐付けられた値も取り出せます。
for (var key: myMap.keySet()) { var value = myMap.get(key); ・・・ }
2つ目のコレクション・ビューであるvalues()を用いれば、すべての値をキーを介さずに取り出すことも可能です。
for (var value: myMap.values()) { ・・・ }
3つ目のコレクション・ビューであるentrySet()で、マッピング(キーと値のペア)をすべて取得する方法もあります。この場合は、次のようにループ中でキーと値を取り出すことが可能です。
for (var entry: myMap.entrySet()) { var key = entry.getKey(); var value = entry.getValue(); ・・・ }
それでは、Mapに格納した値をすべて取り出すプログラムを実際に動作させてみましょう。ここでは、keySet()を用いた例を示します。
サンプルコード:
import java.util.HashMap; public class Main { public static void main(String[] args) { // Mapを作成 var myMap = new HashMap<String, String>(); // Mapに値を格納 myMap.put("apple", "りんご"); myMap.put("grape", "ぶどう"); myMap.put("orange", "みかん"); // Mapの値を取り出して表示 for (var key: myMap.keySet()) { var value = myMap.get(key); System.out.println("キー;" + key + " / 値:" + value); } } }
実行結果:
キー;orange / 値:みかん キー;apple / 値:りんご キー;grape / 値:ぶどう
このように、ループを使えばMapの内容をまとめて処理することが可能です。
なお、今回のサンプルコードでは「for-eachループ」と呼ばれるループを用いました。Javaのループには、ほかにも種類があります。詳しくはこちらの記事も参考にしてみてください。
JavaのforEachとは?コレクションをループで処理する2つの方法
Mapの内容をソートして取得する
Mapをループで処理する上記サンプルコードの実行結果を見て、表示順がソートされていないことに気付いた人もいるのではないでしょうか。これは、Mapではデータの順序が保証されないために起こる現象です。順序を制御するには、例えばkeySet()でキーのコレクションを取得したあと、自分でソートしてからループする必要があります。
ただし、キーのアルファベット順で処理したい場合については、SortedMapを使うのが手軽でしょう。SortedMapは、Mapと同様にインターフェースとして定義されています。これを実装したクラスとしては、TreeMapが代表的です。
HashMapによるサンプルコードを、TreeMapで書き換えると以下のようになります。
サンプルコード:
import java.util.TreeMap; public class Main { public static void main(String[] args) { // Mapを作成 var myMap = new TreeMap<String, String>(); // Mapに値を格納 myMap.put("apple", "りんご"); myMap.put("grape", "ぶどう"); myMap.put("orange", "みかん"); // Mapの値を取り出して表示 for (var key: myMap.keySet()) { var value = myMap.get(key); System.out.println("キー;" + key + " / 値:" + value); } } }
実行結果:
キー;apple / 値:りんご キー;grape / 値:ぶどう キー;orange / 値:みかん
結果がキーでソートされたことがわかるでしょうか。
SortedMapはMapの一種であるため、MapでできることはSortedMapでも可能だと考えておおむね間違いありません。HashMapをTreeMapに差し替えるだけでほぼ同じように使用でき、内容は常にキーでソートされているということです。
なお、通常はTreeMapよりもHashMapを使ったほうがプログラムの動作は高速になります。基本的にはHashMapを使うようにし、ループの順序が重要な場合はTreeMapの使用を検討すればよいでしょう。
値の上書きを避ける
Mapのキーは重複しないため、既存のキーに対して値を格納すると、もとの値は上書きされます。
myMap.put("apple", "りんご"); myMap.put("apple", "リンゴ"); // 既存のキーで上書き
しかし、上書きを避けたい場合もあるでしょう。それには次のメソッドで、値を格納する前にキーの存在を確認する方法が考えられます。
・containsKey(key):キーが存在する場合はtrueを返す
具体的には、次のようにすれば上書きを避けることが可能です。
if (!myMap.containsKey("apple")) { // キーが存在しないことを確認してから myMap.put("apple", "林檎"); // そのキーと値を設定 }
JavaのMapには、これと同様のことを簡単に行なえるメソッドもあります。
・putIfAbsent(key, value):キーが存在しない場合に限り、put(key, value)と同様にキーと値を格納する
つまり、次のようなシンプルな記述で、上書きを避けつつ値を格納できるということです。
myMap.putIfAbsent("apple", "林檎"); // キーが存在しない場合のみ値を設定
では、実際に動作するプログラムで、上書きする場合としない場合の動作を確認しておきましょう。
サンプルコード:
import java.util.HashMap; public class Main { public static void main(String[] args) { // Mapを作成 var myMap = new HashMap<String, String>(); // Mapに値を格納 myMap.put("apple", "りんご"); myMap.put("grape", "ぶどう"); // 既存のキーで値を上書き myMap.put("apple", "リンゴ"); // キーが存在しない場合のみ値を設定 myMap.putIfAbsent("apple", "林檎"); myMap.putIfAbsent("grape", "葡萄"); myMap.putIfAbsent("orange", "蜜柑"); // Mapの値を取り出して表示 for (var key: myMap.keySet()) { var value = myMap.get(key); System.out.println("キー;" + key + " / 値:" + value); } } }
実行結果:
キー;orange / 値:蜜柑 キー;apple / 値:リンゴ キー;grape / 値:ぶどう
値がnullになるのを避ける
Mapに格納した値は、次のメソッドで削除できます。
・remove(key):キーと、キーに紐付けられた値を削除する
では、削除済みのキーや最初から存在しないキーを指定して値を取り出そうとしたら、結果はどうなるのでしょうか。次のプログラムで、動作を確認してみましょう。
サンプルコード:
import java.util.HashMap; public class Main { public static void main(String[] args) { // Mapを作成 var myMap = new HashMap<String, String>(); // Mapに値を格納 myMap.put("apple", "りんご"); myMap.put("grape", "ぶどう"); // Mapから値を削除 myMap.remove("grape"); // キーの有無と値の関係を確認 { var value = myMap.get("apple"); System.out.println("キー;apple / 値:" + value); } { var value = myMap.get("grape"); System.out.println("キー;grape / 値:" + value); } { var value = myMap.get("orange"); System.out.println("キー;orange / 値:" + value); } } }
実行結果:
キー;apple / 値:りんご キー;grape / 値:null キー;orange / 値:null
キーが存在しなければ、値はnullになることがわかるでしょう。
nullはできる限り放置せず、早めに対処するのがおすすめです。それには次のようにして、値がnullだった場合はデフォルト値を割り当てる方法が考えられます。
var value = myMap.get("grape"); if (value == null) { value = "葡萄"; }
JavaのMapには、より簡単に同様のことができるメソッドもあります。
・getOrDefault(key, defaultValue):get(key)と同様に値を取り出すが、キーが存在しない場合はdefaultValueを返す
これにより、次のようなシンプルな記述で値がnullになるのを避けることが可能です。
var value = myMap.getOrDefault("grape", "葡萄");
実際に動作するプログラムで、存在しないキーに対してデフォルト値が割り当てられる様子を確認しておきましょう。
サンプルコード:
import java.util.HashMap; public class Main { public static void main(String[] args) { // Mapを作成 var myMap = new HashMap<String, String>(); // Mapに値を格納 myMap.put("apple", "りんご"); myMap.put("grape", "ぶどう"); // Mapから値を削除 myMap.remove("grape"); // キーの有無と値の関係を確認 { var value = myMap.getOrDefault("apple", "林檎"); System.out.println("キー;apple / 値:" + value); } { var value = myMap.getOrDefault("grape", "葡萄"); System.out.println("キー;grape / 値:" + value); } { var value = myMap.getOrDefault("orange", "蜜柑"); System.out.println("キー;orange / 値:" + value); } } }
実行結果:
キー;apple / 値:りんご キー;grape / 値:葡萄 キー;orange / 値:蜜柑
なお、このようにnullに対処するのは、放置すると例外の発生などのリスクがあるためです。Javaの例外については、こちらの記事で解説しているので参考にしてください。
Javaのtry-catchとは?例外処理の方法をサンプルコードで解説
JavaのMapでキーと値のデータ構造を活用しよう
JavaのMapは、指定した型のキーと値を紐付けて格納できるデータ構造です。HashMapやTreeMapなどの種類がありますが、いずれも共通のインターフェースで使用できます。Mapを活用すれば、開発の幅も広がるでしょう。今回紹介したサンプルコードも参考にしながら、ぜひプログラミングに取り入れてみてください。