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)

// 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円
横綱弁当: 見つかりませんでした
例題の解答例