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
コマンドで追加していくことで,結果が変わってきます.
まとめ
Map
Map
とは,キーとバリューのペアの集合を扱う型.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
を利用してください.
- 文字列を特定の文字で区切る(