2017-06-08 第9回目 Map
本日のテーマ
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)
Complex complex1 = map.get("バリューは同じものでも良い");
Complex complex2 = map.get("この文字列ならなんでも良い");
Complex complex3 = map.get("対応付けがない場合はnullが返される");
上のサンプルプログラムのように,getメソッドを呼び出すことで,Map内の
キーに対応付けられたバリューを取得できます.
返り値の型は,HashMap型の変数宣言時に指定した型(この例ではComplex型)でなければいけません.
キーに対応付けられたバリューが存在しない場合,nullが返されます.
上のサンプルプログラムでは,3行目のキーはバリューとの対応付けがないため,nullが返されます.
なお,返り値が nullの時にデフォルト値を返す getOrDefault メソッドも用意されています.
Complex complex4 = map.getOrDefault("対応付けがない時のデフォルト値を返す", c1); // c1 が返される.
// 上記のプログラムは以下の4行のプログラムと同等.
Complex complex5 = map.get("対応付けがない時のデフォルト値を返す");
if(complex5 == null){ // キーに対応する値がある場合は,この if 文の中身は実行されない.
complex5 = c1;
}
Mapにあるデータを削除する (remove)
map.remove("1+i");
Complex removedValue1 = map.remove("バリューは同じものでも良い");
キーを指定して,バリューとの対応付けを削除します. 削除すると,対応付けられていたバリューが返り値として返されます. 上のサンプルプログラムの1行目のように,返り値を無視しても構いません.
指定したキーに対応付けられたバリューが存在しなかった場合,nullが返されます.
Mapのサイズを取得する (size)
Integer size = map.size();
Mapの現在のサイズを取得したい場合は,sizeメソッドを利用しましょう.
キーとバリューのペアがいくつ格納されているかが得られます.
Mapの要素の繰り返し (Iterator)
Java言語では,Mapの繰り返しは,次の2種類が利用できます.
注意すべき点として,Mapには順番が存在しないため,従来の Integer型を用いる繰り返しは行えません.
そのため,Iterator型を利用して繰り返しを行う必要があります.
このとき,java.util.Mapをインポートしておく必要があります(import java.util.Map;).Map.Entry
型をHashMapの要素として扱うためです.
- 一番典型的な
Iterator型を利用する方法です.Javaらしい書き方です. ただし,ループの途中でmapの要素数を変化させることは 混乱の元になるので,ループ内でのMapへの値の追加・削除は行わない方が良いでしょう.-
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(); // ここに繰り返しの処理を書く. }
-
- 拡張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 = new Integer(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を用いればキーでソートされた結果が得られます.
サンプルプログラムを変更して確認してみましょう.
例題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円
横綱弁当: 見つかりませんでした
練習問題
1. CSVデータの格納
ここでは,郵便番号から住所の検索を行うプログラムを作成します.
- 郵便番号データを用意してください.
- 郵便番号データダウンロードページから自分の出身地の郵便番号データをダウンロードしてください.
- ダウンロードしたzipファイルを解凍してください.
- 解凍してできたファイルを
zipcode.csvにファイル名を変更してください. - ダウンロードしたファイルの文字コードは ShiftJIS になっています.utf-8 でなければJavaでは文字化けを起こしますので,変換しておいてください.
nkfコマンドがインストールされているなら,ターミナルで次のように入力しましょう.nkf -u --overwrite zipcode.csv
- 各種エディタでも文字コードを変換できます.
- Emacsであれば,ファイルを開いて,
Ctrl+x RET fと入力すると変換したい文字コードを聞かれますので,utf-8と入力してください. - Atomであれば,AtomでShift-JISのCSVデータをUTF-8に変換するを参照してください.
- Emacsであれば,ファイルを開いて,
- 郵便番号を読み込み,検索を行うプログラムを作成してください.
- クラス名は
ZipCodeとしてください. - コマンドライン引数で与えられた郵便番号に対応する住所を出力してください.
- クラス名は
ヒント
ベースとなるプログラム
例題1. お弁当の料金表が参考になるでしょう.
初期化処理(initializeメソッド)でzipcode.csvを読み込み,HashMap型の変数に
郵便番号と対応した住所を追加(put)しましょう.
その後,検索処理を行います.検索処理はsearchAndPrintメソッドとほぼ同じで良いでしょう.
キーとバリューそれぞれの型は何が良いかを考えて作成しましょう.
ダブルクォートの削除
ダウンロードした csv ファイルは,各カラムがダブルクォートで囲まれています.
これを削除するには,以下のメソッド(stripQuote)を使うと良いでしょう.sampleCodeメソッド
はstripQuoteの使い方の例です.sampleCodeメソッドにあるように,splitで
得られた配列の各要素を順にstripQuoteに渡すことで,
ダブルクォートを除いた部分を取り出しています.
String stripQuote(String item){
if(item.matches("\".*\"")){
return item.substring(1, item.length() - 1);
}
return item;
}
void sampleCode(){
String value = this.stripQuote("\"クォートで囲まれた文字列\"");
// valueには,"クォートで囲まれた文字列"が代入される.
}
出力例
$ java ZipCode 6038035 6038047 1000001
6038035: 京都市北区上賀茂朝露ケ原町
6038047: 京都市北区上賀茂本山
1000001: 見つかりませんでした
100-0001 は東京都の郵便番号ですので,京都府の郵便番号表では見つかりませんので, 上記のように「見つかりませんでした」と出力されています.
2. 電話帳の作成
以下の仕様に従った名前と電話番号のペアを管理する電話帳を作成しましょう.
- クラス名は
PhoneBookとしてください. - 起動するとコマンド入力待ちとなります.
- 以下のコマンドを入力することによって,データの更新が行われる.
add 名前 電話番号- 電話帳に名前と対応する電話番号を追加する.
list- 登録された名前と電話番号の一覧を表示する.
find 名前- 電話帳に名前が存在すれば名前と電話番号を表示する.
- 電話帳に名前が存在しなければ何も表示しない.
remove 名前- 電話帳から名前のデータを削除する.
- 電話帳に名前のデータが存在しなければ何も行わない.
exit- 電話帳を終了する.
- 入力において,文法エラーは起きないものとします.
- 標準入力から1行読み込むには,
SimpleConsole.javaを利用してください.SimpleConsole.javaをダウンロードし,プログラムと同じディレクトリに置いてください.- 次のコードで1行を読み込めます.
-
SimpleConsole console = new SimpleConsole(); String line = console.readLine();
-
- もちろん,
newは1度だけ行えば良いです.
- 次のコードで1行を読み込めます.
- 入力した文字列をスペースで区切るには,
String型のsplitメソッドを利用しましょう.String[] items = line.split(" ");
SimpleConsole.javaは一切変更してはいけません.- 同様に,
PhoneBook.javaには,SimpleConsole.javaのコードを貼り付けてはいけません.
- 同様に,
出力例
> list
> add tamada 090-1111-1111
> find tamada
tamada 090-1111-1111
> find akiyama
> add akiyama 090-2222-2222
> list
tamada 090-1111-1111
akiyama 090-2222-2222
> remove akiyama
> find akiyama
> add tamada 090-1111-2222
> list
tamada 090-1111-2222
> exit
>で始まる行が入力を表しています.
最初は電話帳に何も入力されていないため,listコマンドを入力しても何も出力されません.
データをaddコマンドで追加していくことで,結果が変わってきます.
まとめ
MapMapとは,キーとバリューのペアの集合を扱う型.Mapの種類HashMap: 典型的なMap.TreeMap: キーの辞書順でペアを保持するMap.LinkedHashMap: 追加した順でペアを保持するMap.
Mapの宣言方法HashMap<キーの型, バリューの型> map = new HashMap<キーの型, バリューの型>Mapに格納するキーの型,バリューの型を指定しなければならない.
HashMap<キーの型, バリューの型> map = new HashMap<>- 後ろのキーの型,バリューの型は省略しても良い.
Mapの操作Mapへのデータの追加:map.put(key, value);- すでにキーが登録されている場合,バリューは上書きされる.
Mapからのデータ取得:バリューの型 value = map.get(key);- 対応する値がない時は,
nullが返される. - 対応する値がない時で,
null以外のデフォルト値を返す場合は,getの代わりにgetOrDefaultメソッドを用いる.
- 対応する値がない時は,
Mapからのデータ削除:バリューの型 value = map.remove(key);Mapのサイズ:map.size();Mapの繰り返し-
for(Map.Entry<キーの型, バリューの型> entry: map.entrySet()){ キーの型 key = entry.getKey(); バリューの型 value = entry.getValue(); // ペアについての処理を行う. } Map.Entryを使うには,java.util.Mapのインポートが必要.
-
Mapのサンプルプログラム
- その他
- 文字列を特定の文字で区切る(
split).String[] items = csvString.split(",");csvString = "aaa,bbb, ccc"の場合,上記のコードの結果は,itemsの各要素はそれぞれ,"aaa","bbb"," ccc"の3つのString型に分割される.
- 文字列の特定の部分を抜き出す(
substring)String value = baseString.substring(1, 5);baseString = "The value of baseString"の場合,上記のコードの結果は,"he v"となる.
- 標準入力からのデータの読み込み方法
SimpleConsole.javaを利用してください.
- 文字列を特定の文字で区切る(