2017-06-08 第9回目 Map

本日のテーマ

Map

Mapとは

住所録を作成することを考えます.iPhoneやAndroidの連絡先アプリを想像してください.

住所録のイメージ

住所録では,上記のように住所や電話番号など複数の情報が名前に紐付いています. このように,人の名前をキーとし,その人に関する情報をバリューとして紐付ける方法として,Mapというデータ構造 が利用できます.Mapでは,キーとバリューのペアの集合を扱います.

Java以外の他の言語では,Mapを連想配列やディクショナリなどと呼びます. いずれもある実体に別の実体を関連づけるためのデータ構造です.

別の考え方をすると,Listはインデックスという数値に実体が対応付いていました.Mapは 実体に紐付けるインデックスに相当する部分が,数値以外の自由な値が指定できるものとも考えられできます.

Mapの種類

先ほども述べたようにMapはキーとバリューのペアの集合を扱うデータ構造です. その実現方法に,ハッシュ値を利用する方法,木構造を利用する方法などがあります. それぞれの実現方法で,Mapを実現する型が存在します.HashMapTreeMapの2つの型です. どちらを利用しても,利用方法や,処理結果に違いはありません. 以下の例では,HashMapを利用して説明していますが,HashMapTreeMapに置き換えても 同じ説明が成り立ちますので,適宜読み替えてください.

なお,HashMapを使うときには,import文が 必要です.import java.util.HashMap;クラス宣言の前に書きましょう.

Mapの宣言方法

 Mapを使うには,Listと同じように,データ構造にどのような型の変数を格納するかを 宣言しなければいけません. 例えば,HashMapString型をキーに 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の後ろの格納する型は省略可能です. 以下のコードは,上記のComplexStringをそれぞれキー,バリューに指定したコードと同じプログラムとしてコンパイルされます.

// 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の要素として扱うためです.

  1. 一番典型的な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();
          // ここに繰り返しの処理を書く.
      }
      
  2. 拡張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データの格納

ここでは,郵便番号から住所の検索を行うプログラムを作成します.

  1. 郵便番号データを用意してください.
    • 郵便番号データダウンロードページから自分の出身地の郵便番号データをダウンロードしてください.
    • ダウンロードした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に変換するを参照してください.
  2. 郵便番号を読み込み,検索を行うプログラムを作成してください.
    • クラス名は 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. 電話帳の作成

以下の仕様に従った名前と電話番号のペアを管理する電話帳を作成しましょう.

出力例

> 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コマンドで追加していくことで,結果が変わってきます.

ページのトップに戻る

まとめ