Map(連想配列)
Mapとは
住所録を作成することを考えます.iPhoneやAndroidの連絡先アプリを想像してください.
住所録では,上記のように住所や電話番号など複数の情報が名前に紐付いています.
このように,人の名前をキーとし,その人に関する情報をバリューとして紐付ける方法として,Map
というデータ構造
が利用できます.Map
では,キーとバリューのペアの集合を扱います.
Java以外の他の言語では,Map
を連想配列やディクショナリなどと呼びます.
いずれもある実体に別の実体を関連づけるためのデータ構造です.
別の考え方をすると,List
はインデックスという数値に実体が対応付いていました.Map
は
実体に紐付けるインデックスに相当する部分が,数値以外の自由な値が指定できるものとも考えられできます.
Mapの種類
先ほども述べたようにMap
はキーとバリューのペアの集合を扱うデータ構造です.
その実現方法に,ハッシュ値を利用する方法,木構造を利用する方法などがあります.
それぞれの実現方法で,Map
を実現する型が存在します.HashMap
とTreeMap
の2つの型です.
どちらを利用しても,利用方法や,処理結果に違いはありません.
以下の例では,HashMap
を利用して説明していますが,HashMap
をTreeMap
に置き換えても
同じ説明が成り立ちますので,適宜読み替えてください.
なお,HashMap
を使うときには,import
文が
必要です.import java.util.HashMap;
とクラス宣言の前に書きましょう.
Mapの宣言方法
Map
を使うには,List
と同じように,データ構造にどのような型の変数を格納するかを
宣言しなければいけません.
例えば,HashMap
にString
型をキーに Integer
型をバリューとする場合,次のような宣言が必要です.
HashMap<String, Integer> map =
new HashMap<String, Integer>();
上記のように,HashMap<キーの型, バリューの型>
という型として宣言しなければいけません.
また,実体を作成するときも,new HashMap<キーの型, バリューの型>()
のように作成しなければいけません.
キーの型,バリューの型が異なれば,同じHashMap
型であっても,異なる型であると判断されます.
// Complexをキーに,String型をバリューとした Map.
HashMap<Complex, String> map1 =
new HashMap<Complex, String>();
// String型をキーに,Complex型をバリューとした Map.
HashMap<String, Complex> map2 =
new HashMap<String, Complex>();
つまり,上記のように Complex
型をキーに,String
型を
バリューとしたHashMap
型変数map1
と
キーとバリューの型を逆にしたHashMap
型変数map2
は
キーとバリューの型が異なるため,異なる型であると判断され,互いに代入できません.
なお,List
と同じく,new
の後ろの格納する型は省略可能です.
以下のコードは,上記のComplex
,String
をそれぞれキー,バリューに指定したコードと同じプログラムとしてコンパイルされます.
// Complexをキーに,String型をバリューとした Map.
HashMap<Complex, String> map1 = new HashMap<>();
// String型をキーに,Complex型をバリューとした Map.
HashMap<String, Complex> map2 = new HashMap<>();
なお,上記のように,独自に作成した型(第5講で作成したComplex
型)もMap
に格納可能です.
Mapの操作
Map
に対する操作はList
と同じく,CRUD (Create, Read, Update, Delete)が可能です.
以降の説明は,次に示すHashMap
型の変数に対してメソッドを呼び出すものとして読み進めてください.
HashMap<String, Complex> map = new HashMap<>();
// HashMap<String, Complex> map =
// new HashMap<String, Complex>();
// 上記のように書いても良い.
Mapにデータを追加する (put)
Complex c1 = // Complexの実体を作成する.
Complex c2 = // Complexの実体を作成する.
map.put("1+i", c1);
map.put("この場合文字列ならなんでも良い", c2);
map.put("バリューは同じものでも良い", c1);
map.put("1+i", c2); // "1+i"に紐付いているバリューを更新する.
データを追加するには,上のサンプルプログラムのようにput
メソッドを用います.put
メソッドで追加するときのキーの型とバリューの型は,Map
の生成時に指定した型と合わせる必要があります.
理論上はデータ数に上限はなく,幾つでも追加できます.
また,異なるキーに同一のバリューを割り当てられます.一方で,同じキーに異なるバリューは割り当てられません. すでにキーとバリューのペアが割り当てられているところに,別のバリューを割り当て用とすると, 今までのバリューが上書きされます.
Mapにあるデータを取得する (get)
// putで追加した c1 が返される.
Complex complex1 = map.get("バリューは同じものでも良い");
// putで追加した c1 が返される.
Complex complex2 = map.get("この場合文字列ならなんでも良い");
// 対応付けがないため,null が返される.
Complex complex3 = map.get("対応付けがない場合はnull");
上のサンプルプログラムのように,get
メソッドを呼び出すことで,Map
内の
キーに対応付けられたバリューを取得できます.
返り値の型は,HashMap
型の変数宣言時に指定した型(この例ではComplex
型)でなければいけません.
キーに対応付けられたバリューが存在しない場合,null
が返されます.
上のサンプルプログラムでは,3行目のキーはバリューとの対応付けがないため,null
が返されます.
なお,返り値が null
の時にデフォルト値を返す getOrDefault
メソッドも用意されています.
// c1 が返される.
Complex complex4 =
map.getOrDefault("対応付けがない場合", c1);
// 上記のプログラムは以下の4行のプログラムと同等.
Complex complex5 = map.get("対応付けがない場合");
if(complex5 == null){
// キーに対応する値がある場合,このif文は実行されない.
complex5 = c1;
}
Mapにあるデータを削除する (remove)
map.remove("1+i");
// 紐付けが解消されたので,nullが返される.
Complex removedValue1 = map.remove("1+i");
キーを指定して,バリューとの対応付けを削除します. 削除すると,対応付けられていたバリューが返り値として返されます. 上のサンプルプログラムの1行目のように,返り値を無視しても構いません.
指定したキーに対応付けられたバリューが存在しなかった場合,null
が返されます.
Mapのサイズを取得する (size)
Integer size = map.size();
Map
の現在のサイズを取得したい場合は,size
メソッドを利用しましょう.
キーとバリューのペアがいくつ格納されているかが得られます.
Mapの要素の繰り返し
Java言語では,Map
の繰り返しは,次の2種類が利用できます.
注意すべき点として,Map
には順番が存在しないため,従来の Integer
型を用いる繰り返しは行えません.
そのため,Iterator
型を利用して繰り返しを行う必要があります.
このとき,java.util.Iterator
だけでなく,java.util.Map
もインポートしておく必要があります
(import java.util.Iterator;
,import java.util.Map;
).Map.Entry
型をHashMap
の要素として扱うためです.
典型的な方法(Iterator)
一番典型的なIterator
型を利用する方法です.Javaらしい書き方です.
for(Iterator<Map.Entry<String, Complex>> iterator =
map.entrySet().iterator();
iterator.hasNext(); ){
Map.Entry<String, Complex> entry = iterator.next();
String key = entry.getKey();
Complex value = entry.getValue();
// ここに繰り返しの処理を書く.
}
ただし,map
のループの途中で map
の要素数を変化させると例外(ConcurrentModificationException
)が発生する場合があります.
ループを回している対象の Map
への値の追加(get
)・削除(remove
)は行わないようにしましょう.
for(Iterator<Map.Entry<String, Complex>> iterator =
map.entrySet().iterator();
iterator.hasNext(); ){
Map.Entry<String, Complex> entry = iterator.next();
String key = entry.getKey();
Complex value = entry.getValue();
// mapを対象にループを回しているため,
// ここで,mapの要素数を変化させる(get, removeなど)と例外が投げられる.
}
拡張for文を使った方法
拡張for文と呼ばれる書き方.実質的には,Iterator
型を利用する方法と同じです.
コンパイラがIterator
型を利用する方法に変換してコンパイルしてくれます.
最近のJavaの書き方はこの方法です.
for(Map.Entry<String, Complex> entry: list.entrySet()){
String key = entry.getKey();
Complex value = entry.getValue();
// ここに繰り返しの処理を書く.
}
上記のように,Map.Entry
型を順番に取り出して処理するようなプログラムに
なっています.Map.Entry
型はキーとバリューのペアを扱う型です.
すなわち,Map
に格納されている各ペアを扱う型です.Map.Entry
型の
利用には,import java.util.Map;
というimport
文が必要です.
サンプルプログラム
コマンドライン引数の文字列をコンマ(,
)で区切り,前方の値をキーに,
後ろの値をバリューにしてMap
に格納するプログラム.
最後に,順番に取り出し,出力しています.
import java.util.HashMap;
import java.util.Map;
public class HashMapSample{
void run(String[] args){
HashMap<String, Integer> map = new HashMap<>();
for(String arg: args){
this.putValueToMap(map, arg);
}
this.printMap(map);
}
void putValueToMap(HashMap<String, Integer> map,
String string){
// , で文字列を区切っている.
String[] items = string.split(",");
Integer value = Integer.valueOf(items[1]);
map.put(items[0], value);
}
void printMap(HashMap<String, Integer> map){
for(Map.Entry<String, Integer> entry:
map.entrySet()){
System.out.printf("%s: %d%n",
entry.getKey(), entry.getValue());
}
}
// mainメソッドは省略.
}
出力例
$ java HashMapSample one,1 two,2 three,3 four,4
four: 4
one: 1
two: 2
three: 3
先にも述べたようにMap
は順序を保持しないため,追加順と出力順が異なっている点に注意しましょう.
出力の順序には意味はありません.実際には,キーのハッシュ値の順番になっていますが,
その値を意識する必要はありません.
もし,追加順に出力したい場合は,HashMap
の代わりにLinkedHashMap
を利用してください.
また,HashMap
の代わりに TreeMap
を用いればキーでソートされた結果が得られます.
サンプルプログラムを変更して確認してみましょう.
文字列を分割する
文字列を特定の文字で分割するには,String
型のsplit
メソッドを利用します.
split
の引数には分割したい区切り文字を指定します.
返り値は引数で渡された文字で分割された結果が,String
型の配列で返されます.
文字列分割のサンプルプログラム
String string = "abr,aca,dab,ra";
String[] array1 = string.split(",");
System.out.println(Arrays.toString(array1);
// => [ "abr", "aca", "dab", "ra" ]
String[] array2 = string.split("a");
System.out.println(Arrays.toString(array2);
// => ["", "br,", "c", ",d", "b,r"]
上記のように,"abr,aca,dab,ra"
を ","
で分割した場合は,長さ4の配列が返されます.
この時のそれぞれの要素は上記のサンプルプログラムの通りです.
同様に,"abr,aca,dab,ra"
を "a"
で分割した場合は,長さ5の配列が返されます.
コンマがややこしいですが,上記の実行結果をよく確認してください.
このように,指定された文字列で分割した結果を得るには split
メソッドを利用します.
例題1. お弁当の料金表
以下のプログラムの欠けている部分を補って,指定したお弁当の料金を出力するプログラムを作成してください.
import java.util.HashMap;
public class LunchBoxPrices{
HashMap<String, Integer> prices;
void run(String[] args){
this.initialize();
for(String arg: args){
this.searchAndPrint(arg);
}
}
void searchAndPrint(String lunchBoxName){
Integer price = // お弁当の料金を prices から取得(get)する.
if(price == null){ // お弁当が見つからなかった.
// ここに見つからなかった旨を出力する処理を書く.
}
else{
// お弁当の料金を出力する.
System.out.printf("%s: %d円%n", lunchBoxName, price);
}
}
void initialize(){ // お弁当の料金表を作成するメソッド.
this.prices = new HashMap<>();
prices.put("日の丸弁当", 200);
// 他のお弁当の料金も追加する(10個程度).
}
// mainメソッドは省略.
}
お弁当の料金,名前は適当に決めてください.
出力例
$ java LunchBoxPrices のり弁当 上幕ノ内弁当 横綱弁当
のり弁当: 350円
上幕ノ内弁当: 800円
横綱弁当: 見つかりませんでした