javaのマップはユニークな(重複できない)キーに対して値を対応させるものです。キーと値は作成時にそれぞれクラスを指定します。
たとえば、
Map<String,String> map1 = new LinkedHashMap<>(); Map<String,Double> map2 = new LinkedHashMap<>();
MapとLinkedHashMapの関係はListとLinkedListの関係と同じです。
宣言は<>の中に入れるkeyとvalueのクラスをそれぞれ宣言します。後ろの<>はこのように省略できます。
intやdoubleなどの基本変数は、<Integer>, <Double> と相当するラッパークラスで宣言します。
自作のクラスや、配列やリストも使えます。整数と整数からなるリストを使う場合は次のようになります。
Map<Integer,List<Integer>> sgmss = new LinkedHashMap<>();
javascriptの連想配列では、キーは文字列のみ、値の方には様々な型の変数やオブジェクトを混ぜて格納できましたが、javaでは宣言時にクラスを限定します。もちろんObjectを指定して、混ぜてしまうことも不可能ではありませんが、普通はやりません。
キーと値の組をエントリーといいます。宣言の後、追加して格納します。
import java.util.Map; import java.util.LinkedHashMap; class MapTest01{ public static void main( String[] args ) { Map<String,String> map = new LinkedHashMap<>(); map.put("apple","りんご"); map.put("orange","みかん"); map.put("peach","もも"); System.out.println(map.get("orange")); System.out.println(map.toString()); map.put("orange","オレンジ"); System.out.println(map.get("orange")); System.out.println(map.toString()); } }
実行結果
みかん {apple=りんご, orange=みかん, peach=もも} オレンジ {apple=りんご, orange=オレンジ, peach=もも}
put()は要素の追加ですが、同一のkeyが既にあればvalueを上書きします。
参照はget(key)で行います。
for-each と言われながら、予約語としてはやっぱり forを使うのは 拡張for文です。
Mapの場合は、配列やリストではそのまま:の後ろに書きましたが、Mapでは、キーを使う方法と値を使う方法、両方をセットにしたentryを使う方法から選べます。
3ついっぺんに使ってみます。
import java.util.Map; import java.util.LinkedHashMap; class MapTest02{ public static void main( String[] args ) { Map<String,String> map = new LinkedHashMap<>(); map.put("apple","りんご"); map.put("orange","みかん"); map.put("peach","もも"); System.out.println("要素数:"+map.size()); System.out.println("(1) using keySet()"); for (String key : map.keySet()) { System.out.println("Key:"+key+" Value:" + map.get(key)); } System.out.println("(2) using values()"); for (String value : map.values()) { System.out.println("Value: " + value); } System.out.println("(3) using entrySet()"); for (Map.Entry<String,String> entry : map.entrySet()) { System.out.print("Key:" + entry.getKey() ); System.out.println(" Value:" + entry.getValue()); } }
実行結果
要素数:3 (1) using keySet() Key:apple Value:りんご Key:orange Value:みかん Key:peach Value:もも (2) using values() Value: りんご Value: みかん Value: もも (3) using entrySet() Key:apple Value:りんご Key:orange Value:みかん Key:peach Value:もも
:の前に書くインスタンスのクラスはMapの要素により決まります。
:の前に書く変数のクラス | メソッドの戻値のクラス | メソッド |
---|---|---|
K | Set<K> | keySet() |
V | Collection<V> | values() |
Map.Entry<K,V> | Set<Map.Entry<K,V>> | entrySet() |
Java8から使用できるメソッドです。
import java.util.Map; import java.util.LinkedHashMap; class MapTest03{ public static void main( String[] args ) { Map<String,String> map = new LinkedHashMap<>(); map.put("apple","りんご"); map.put("orange","みかん"); map.put("peach","もも"); System.out.println("要素数:"+map.size()); System.out.println("(4) using forEach()"); map.forEach((k, v) -> System.out.println("Key:"+k+" Value:" + v)); } }
実行結果
要素数:3 (4) using forEach() Key:apple Value:りんご Key:orange Value:みかん Key:peach Value:もも
javascript版と同じ企画です。
連想配列に出会ったのは1993年のことでした。UNIX上のawkでした。
次のような名前と数値が組になったデータから、名前ごとの合計を出すというものです。
onion 50 carrot 226 carrot 315 eggplant 30 radish 75 eggplant 30 onion 120 carrot 417 eggplant 23 onion 80 radish 89 carrot 715 onion 95 radish 135 radish 98
こんな感じでできてしまいます。
$ awk '{sum[$1]+=$2} END {for(name in sum)print name,sum[name]}' onion.txt radish 397 eggplant 83 carrot 1673 onion 345
END以下は結果の表示のためのforで、計算の本体は連想配列 sum[$1] += $2 です。 $1がキー、$2が値でキーが同じ場所に値を加算するものです。
これを java でやってみます。
awkではデータはファイルですが、javascriptでは別ファイルにするのは無理があるのでプログラム中に書き込みました。Javaではファイルから読む方法に戻します。Files.readAllLines()を使ってみます。
データファイルは、上記の onion 50\n carrot 226\n carrot 315\n... とスペース区切りのテキストファイル。onion.txt という名前でプログラムを実行するディレクトリに置きます。
import java.util.Map; import java.util.LinkedHashMap; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; class MapTest04{ public static void main( String[] args ) { Path path = Paths.get("onion.txt"); List<String> list = null; try { list = Files.readAllLines(path); } catch (IOException e) { System.err.println( e); } Map<String,Integer> map = new LinkedHashMap<>(); for (String line : list) { String[] tmp = line.split(" "); if (2>tmp.length) continue; String key = tmp[0]; int val = map.getOrDefault(key, 0); val += Integer.parseInt(tmp[1]); map.put(key,val); } map.forEach((k, v) -> System.out.println(k+": " + v)); } }
実行結果
onion: 345 carrot: 1673 eggplant: 83 radish: 397
処理がわかりやすいように冗長に分けて記述しています。
ポイントは、map.getOrDefault(key, 0);です。相当するキーがないとき、戻値はnullですが、これを第二引数の0にするメソッドです。
2>tmp.lengthはデータファイルに空行が入ってしまった時に面倒なので入れてあります。
DebianのパッケージのJavaを使ってきた関係で、最近やっとversion1.8を使うようになりました。11が出たというのに遅いですが、そう急ぐ必要もありません。
今回.getOrDefault()の他にMapにいろいろなメソッドが追加されていることを知り、ちょっと調べてみました。今回の用途ではcompute()関係かなと思っていたのですが、merge()メソッドも使えます。名前から複数のMapの合成をイメージしてしまうのですが、これが邪魔をしていました。
import java.util.Map; import java.util.LinkedHashMap; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; class MapTest05{ public static void main( String[] args ) { Path path = Paths.get("onion.txt"); List<String> list = null; try { list = Files.readAllLines(path); } catch (IOException e) { System.err.println( e); } Map<String,Integer> map = new LinkedHashMap<>(); for (String line : list) { String[] tmp = line.split(" "); if (2>tmp.length) continue; String key = tmp[0]; map.merge( key, Integer.parseInt(tmp[1]), (val, tmp1) -> val+tmp1 ); } map.forEach((k, v) -> System.out.println(k+": " + v)); } }
実行結果
onion: 345 carrot: 1673 eggplant: 83 radish: 397
getOrDefault()と比べて長さとしてはあまり違いはありませんが、ちょっとわかりづらい。
mergeは引数を3つとります。
compute()も使ってみます。引数は2つ。2つめの(k,v)は今度は第一引数のkeyと、それに対応する値のvです。keyがなければvはnullになります。->の後の関数はvがnullかどうかでどちらかの値を返す3項演算子になっています。
import java.util.Map; import java.util.LinkedHashMap; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.List; class MapTest06{ public static void main( String[] args ) { Path path = Paths.get("onion.txt"); List<String> list = null; try { list = Files.readAllLines(path); } catch (IOException e) { System.err.println( e); } Map<String,Integer> map = new LinkedHashMap<>(); for (String line : list) { String[] tmp = line.split(" "); if (2>tmp.length) continue; String key = tmp[0]; int val = Integer.parseInt(tmp[1]); map.compute(key, (k,v) -> (v==null) ? val : v+val); } map.forEach((k, v) -> System.out.println(k+": " + v)); } }
実行結果
onion: 345 carrot: 1673 eggplant: 83 radish: 397
getOrDefault()を使った場合と、merge()、compute()を使った場合を並べてみました。
String key = tmp[0]; int value = Integer.parseInt(tmp[1]); map.put(key, map.getOrDefault(key,0)+value); map.merge(key, value, (v, tmp1) -> v+tmp1 ); map.compute(key, (k,v) -> (v==null) ? value : v+value );
今回の目的にはmerge()を使った方がわかりやすいとは言えない気がしますが、valueの値を一度書けばよいのはメリットかもしれません。
merge(),compute()は、C言語やJava7まででは見慣れないのですが、Java8の拡張の目玉であるラムダ式に絡んだものなので、慣れると応用範囲が広がるのかもしれません。
getOrDefault(),merge(),compute()などが出て、containsKey()を使う機会が出てきませんでした。その他も使用頻度は低そうです。
Java8で随分と新しいメソッドが増えていて、追加と置換を分けて行うことができるようになっています。上記の「エントリーの追加と更新・参照」を書いた時はreplace()が追加されていることを知りませんでした。
v8で 追加 |
メソッド名(引数) | 対象が存在しない時 | 対象が存在する時 | ||
---|---|---|---|---|---|
動作 | 戻値 | 動作 | 戻値 | ||
- | put(K key, V value) | 追加 | null | 置換 | 以前の値 |
v8 | putIfAbsent(K key, V value) | 追加 | null | - | 現在の値 |
- | remove(Object key) | - | null | 削除 | 以前の値 |
v8 | remove(Object key, Object value) | - | false | 削除 | true |
v8 | replace(K key, V value) | - | null | 置換 | 以前の値 |
v8 | replace(K key, V oldValue, V newValue) | - | false | 置換 | true |
ここでは、数値の場合の大きさの順や文字列の文字コード順などデフォルトで設定されているような、単純なものについてやってみます。API仕様にはこの順序を示す natural ordering (自然順序付け) という言葉は出てきますが、きちんと説明している箇所を見つけられないでいます。
Mapインターフェースを実装するクラスは多数あるものの、よく使われるのは HashMap, LinkedHashMap, TreeMap の3つです。
HashMapは登録の順序が保存されないので、その分処理が早くなると期待できます。登録の順を保持したければLinkedHashMapを使います。keyによる並べ替えをしたければ TreeMap です。
これは、put時に二分木を使っておいて、拡張for文やforEach()メソッドで出す時に順番になるようにするものです。後からある種のメソッドで並び替えるというものではありません。
TreeMapを使うときには、SortedMap<K,V>インターフェースで宣言すると機能が増えますが、拡張for文やforEach()メソッドで使うだけならMapでかまいません。
次のプログラムはMapのインスタンスを、 HashMap, LinkedHashMap, TreeMap でそれぞれ生成するように書き換えて試すようになっています。現在の状態はTreeMapです。
データは文字列でASCIIの範囲ですが、ひとつだけ大文字で始まるものがあります。"自然順序付け"です。
import java.util.Map; import java.util.LinkedHashMap; import java.util.HashMap; import java.util.TreeMap; //import java.nio.charset.StandardCharsets; class MapTest07{ public static void main( String[] args ) { //Map<String,String> map = new LinkedHashMap<>(); //Map<String,String> map = new HashMap<>(); Map<String,String> map = new TreeMap<>(); map.put("apple","りんご"); map.put("orange","みかん"); map.put("peach","もも"); map.put("Peach","桃"); map.put("blue", "青"); map.put("yellow", "黄"); map.put("green", "緑"); map.put("red", "赤"); map.put("white", "白"); map.put("black", "黒"); System.out.println("要素数:"+map.size()); map.forEach((k, v) -> System.out.println("Key:"+k+" Value:" + v)); } }
実行結果
まず、LinkedHashMapです。プログラム中の登録順そのままです。
要素数:10 Key:apple Value:りんご Key:orange Value:みかん Key:peach Value:もも Key:Peach Value:桃 Key:blue Value:青 Key:yellow Value:黄 Key:green Value:緑 Key:red Value:赤 Key:white Value:白 Key:black Value:黒
HashMapです。キーを探しやすいようにHashというものを作って格納しますが、コンピュータが探しやすいという都合で取り出す時の順番が変わってしまいます。
要素数:10 Key:orange Value:みかん Key:red Value:赤 Key:apple Value:りんご Key:green Value:緑 Key:blue Value:青 Key:white Value:白 Key:Peach Value:桃 Key:peach Value:もも Key:yellow Value:黄 Key:black Value:黒
TreeMapです。そもそも登録の時に大きさの順で格納位置を探しながら格納します。HashでなくTreeという手法を使っているのです。知っている人は名前から性質を知ることができます。文字列の順番は文字コード順が一番簡単です。大文字が小さな文字コードに割り当てられているのでこのようになります。
要素数:10 Key:Peach Value:桃 Key:apple Value:りんご Key:black Value:黒 Key:blue Value:青 Key:green Value:緑 Key:orange Value:みかん Key:peach Value:もも Key:red Value:赤 Key:white Value:白 Key:yellow Value:黄
大文字小文字を同一視する方法もあります。
Map<String,String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
ただし、Peachとpeachを同一視するので、キーは先に登録される「peach」になり、値は後から登録される「桃」になって要素数は9になってしまいます。
同一視してもキーがユニークになる場合に使うのがいいでしょう。そうでない場合は、区別したままMapを作り、keySet()やentrySet()などでキーを取り出してから別な方法でソートする方法が使えると思います。
逆順にしたければ、
Map<String,String> map = new TreeMap<>(Collections.reverseOrder()); Map<String,String> map = new TreeMap<>(Comparator.reverseOrder()); //1.8なら
大文字小文字を同一視して、なおかつ逆順にしたければ、
Map<String,String> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER.reversed());
ソートはいろいろな方法があって、もっと複雑な指定もできます。
1.8になって楽になったようですので、古い方法を苦労して覚えなくてもいいでしょう。