こんにちは!fumikoです。
大流行のインフルエンザも収束しはじめましたね。
日が長くなってきている今日のごろ、皆様はjava使いになるために日夜努力されていることでしょう。
今回は、ListとMapについて、ちょっとディープな追求をしてみます。
javaは複数のデータをまとめて扱う機能がいくつかあります。
配列とListクラスそしてMapクラスです。
この配列とListクラスそしてMapクラスの共通点は、同じ型のデータを複数の値を格納することができることです。
では違いは何かというと、格納できる型が異なります。
配列はプリミティブ型と言われている、int型、char型やクラス型もつくれます。
しかし、ListとMapはクラスですからプリミティブ型のListやMapをつくることができません。
そして、ListとMapはデータの格納の仕方が異なるため、データの扱い方も異なります。
ではListクラスとMapクラスは何が異なるのでしょうか?
この記事では、ListクラスとMapクラスについて
・ListとMapの違い
・Java8とJava8以前の違い
という基本的な内容から
・ListからMapに変換する方法
・MapからListに変換する方法
など具体的な内容についても解説していきます。
今回はListクラスとMapクラスについて、わかりやすく解説します!
なお、Javaの記事については、こちらにまとめています。
ListとMapの違い
簡単に言うとListとMapの違いは、自分でデータを取り出す仕掛けをつくれるか、それとも、つくれないか、という違いです。
言語仕様として自動的にデータを取り出す仕組みをつくるのがListです。
これに対し、自分でデータを取り出す仕掛けをつくれるのがMapです。
ListとMapのインスタンス生成を見てみましょう。
JDKのバージョンによって書き方が違います。
List | Map | |
---|---|---|
JDK1.4以前 | List list = new ArrayList(); | Map map = new HashMap(); |
JDK1.5以降 | List<String> list = new ArrayList<String>(); | Map<String, Integer> map = new HashMap<String, Integer>(); |
JDK1.5以降の記述形式の場合、右辺の要素の型の指定は以下のように省略することができます。
List<String> list = new ArrayList<>();
Listクラスは格納した順に自動的にindexが生成され、値と紐づけされます。
このindex順にしたがってデータを取り出します。
明示的にindexを指定することができますが、Integer型のみでしかつけられません。
それに対して、Mapクラスはindexとなるkeyとそしてデータとなるvalueをそれぞれ定義する必要があります。
そのため、keyにはIntegerクラスだけではなく、Stringクラスなど基本クラスを持つことができます。
Listに比べ柔軟性が高いデータ管理ができます。
しかしながら、mapクラスはプログラマが扱える内容が多い分、操作が多く、情報量が多いことから、Listクラスに比べ処理速度が劣ります。
順次データを取り出すだけの目的であればListが適しています。
keyと関連付けて値を保持する必要がある場合はMapが適しています。
用途によって使い分ける必要があります。
では、ListとMap変換の処理をJava8以前とJava8での記述の違いをコードレビューを通してしっかり理解をしていただきます。
ちょっとディープなJava8の世界に入る準備はできましたか?
ListとMapの変換とJava8
しつこいですが、ListとMapの最大の違いは、値を任意に取り出すためのkeyを自分で指定できるかどうかです。
インスタンス生成の記述を見ても、ListからMapに変換するには必ずKeyを指定し、そのkeyに紐づける値を設定する必要があります。
そのため、ListからMapに変換する場合、keyのリストとkeyに紐づく値がセットで生成するように設計する必要があります。
Java8とJava8以前の違い
java.utilクラスの設計にはSDK公開より非常に定評がありました。
そのなかで、この煩雑な操作をより簡易な記述に改善する要望が多かったことは事実です。
Java史上最大の変更と言われるラムダ式とStreamへの対応がJava8では行われました。
これはあくまでJava言語の文法レベルのものです。
なぜ、Java8はラムダ式とStreamに対応したのでしょうか?
その背景には、JRubyやGroovy、Jythonなど、java上で動く軽量言語が登場し、対応せざるを得ない状況になったためです。
仕様変更は、過去に作られた膨大なプログラムが動くようなしくみが必要です。
逆にこれらが動かなくなる仕様変更はできません。
そこで、Java8では言語仕様に関する旧バージョンとの相違はコンパイラとランタイムが吸収する仕様になりました。
こうして、より簡易な記述ができるようになりました。
コーディングミスを最小限にとどめる最適な方法は簡易な記述です。
それでは、皆様にもJava8の快適さを体感していただきましょう。
ListからMapに変換する方法
ListからMapへの変換の例として、String型のListを作成し、その後、キーにはListの値を持ち、データは文字列サイズを値にもつMapに変換します。
百聞は一見に如かず!
早速ソースを見てみましょう。
Java8以前
JDK 1.4の場合
Java8以前はListからMapに変換する場合、Listデータを順次読み込みながら、keyと値をMapに追加する操作しかできませんでした。
import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class Main { public static void main(String[] args) { List list = Arrays.asList("chocolate", "makaron", "cheesecake", "pudding", "cookie", "pancake"); System.out.println(list.toString()); Map map = new HashMap(); // 1. listの要素をループで取得 for(int i = 0; i < list.size(); i++) { String s = list.get(i).toString(); // 2. 値を取得 map.put(s, s.length()); // 3.マップへ値を追加 } System.out.println(map.toString()); // 4. mapの中身を確認します } }
実行結果:
[chocolate, makaron, cheesecake, pudding, cookie, pancake] {cookie=6, cheesecake=10, makaron=7, pancake=7, pudding=7, chocolate=9}
まず、Listを作成します。
- リストの要素の値を順に取得します。
- 文字列としていったん退避します。
- マップに退避した文字列をkeyにし、length()メソッドで文字列長を取得し、値としてMapにput()メソッドで追加します。
JDK 1.5でGeneric typeを使用する方法
JDK1.5よりGeneric typeが使えるようになったため、若干簡素な記述ができるようになりました。
しかし、処理としてはJDK1.4と同様いったんすべての要素を読み込んで処理する必要があります。
import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class Main { public static void main(String[] args) { List list = Arrays.asList("chocolate", "makaron", "cheesecake", "pudding", "cookie", "pancake"); System.out.println(list.toString()); Map map = new HashMap(); for (Object s : list) { // 1. list要素を順次読み込みます map.put(s.toString(), s.toString().length()); // 2. mapに追加する。 } System.out.println(map.toString()); } }
実行結果:
[chocolate, makaron, cheesecake, pudding, cookie, pancake] {cookie=6, cheesecake=10, makaron=7, pancake=7, pudding=7, chocolate=9}
Java8でStreamを使用する方法
Java史上最大の変更、「ラムダ式とstream対応」がJava8行われたことで「ListからMapに変換する記述」が簡易になりました。
手順は
- Listをstreamにし、
- streamからListをCollectorに渡す
- Mapに変換する
- キーと値を抽出する関数を渡す
となります。
注意!キーが重複すると例外が発生します。
ここでは、文字列をキーにその文字列の長さを値に格納したMapに変換してみます。
以下のような記述になります。
Map<String, Integer> map = list.stream() // 1. listをstream()に渡す。 .collect(Collectors.toMap( // 2. streamをcollect()メソッドに渡し、CollectorsクラスのMapインスタンス生成メソッドを実行します s -> s, // 3. Mapキーを取得するラムダ式 s -> s.length() // 4. Mapの値を取得するラムダ式 ));
サンプルコードで確認してみましょう。
import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<String> list = new ArrayList<>(Arrays.asList("chocolate", "makaron", "cheesecake", "pudding", "cookie", "pancake")); System.out.println(list.toString()); Map<String, Integer> map = list.stream().collect(Collectors.toMap(s -> s, s -> s.length())); System.out.println(map.toString()); } }
実行結果:
[chocolate, makaron, cheesecake, pudding, cookie, pancake] {cookie=6, cheesecake=10, pancake=7, makaron=7, pudding=7, chocolate=9}
Streamでのグルーピング処理
ときには、値やkeyに一定の規則や関連があると思われる場合、ListやMapを使い分けたいこともあります。
そんなときにJava8ではグルーピング処理が使えます。
手順としては、
- 値をグループ分けし、
- グループ分けされた値を集約する関数を渡す
という処理になります。
ここでは、”先頭の一文字が同一の要素をグルーピング”してみます。
// 先頭の一文字が同一の要素をグルーピングする Map<Character, List<String>> map = list.stream() // 1.listをstream()に渡す .collect(Collectors.groupingBy(s -> s.charAt(0))); // 2.collect()メソッドからCollectors.groupingBy()メソッドを渡す
サンプルコードで確認してみましょう。
import java.util.Arrays; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; public class Main { public static void main(String[] args) { List<String> list = new ArrayList<>(Arrays.asList("chocolate", "makaron", "cheesecake", "pudding", "cookie", "pancake")); System.out.println(list.toString()); // 先頭の一文字が同一の要素をグルーピングする Map<Character, List<String>> map = list.stream().collect(Collectors.groupingBy(s -> s.charAt(0))); // 先頭が'c'で始まるものを表示 System.out.println(map.get('c')); // 先頭が'p'で始まるものを表示 System.out.println(map.get('p')); // 先頭が'm'で始まるものを表示 System.out.println(map.get('m')); } }
実行結果:
[chocolate, makaron, cheesecake, pudding, cookie, pancake] [chocolate, cheesecake, cookie] [pudding, pancake] [makaron]
list.stream()メソッドで、listをストリームに渡します。
collect()メソッドにて、map内の要素ごとにグループをつくるメソッドCollectors.groupingBy()メソッドを渡します。
Collectors.groupingBy()メソッドは、mapのkeyにグループごとに識別するための値をセットし、valueにはグループの値を保持しています。
ここでは先頭の一文字が同一の要素をグルーピングしたいため、各要素の先頭をcharAt(0)メソッドにて判定します。
実行はmap.get(‘@’)メソッドで[@]に[a-z]までの任意の英字をセットすることでグループごとのリストを得ることができます。
同様な手順で、
・minByメソッド:ラムダ式が返す数値の最小値を返す
・maxByメソッド:ラムダ式が返す数値の最大値を返す
・partitioningByメソッド:ラムダ式の要素を指定した条件で分割して返す
・joiningByメソッド:要素を区切り文字で連結した文字列を取得する
ということもできます。
余力があればチャレンジしてみて下さい。
MapからListに変換する方法
MapからListに変換するにはMapのkeyとValueをそれぞれ別に渡す必要があります。
Mapのキーのセットを取得するにはMap.keySet()メソッドを使用します。
このメソッドで得られるSetは、元のMapのキーを参照しています(シャロー・コピー)。
keyと値はすべて元のMapと関連付けられています。
そのため、Map.keySet()メソッドを用いて、Setを取得し、そしてMapの内容を変更すると、その、Setの内容もそれに合わせて増減します。
思わぬバグの原因になるため使う際は注意をしてください。
コンストラクタが異なること以外は、Mapからlistへの変換処理の手順はJava8以前とJava8は何ら変わりはありません。
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; public class Main { public static void main(String[] args) { Map map = new HashMap(); map.put("Samurai", "Engineer"); map.put("Soldier", "Warrior"); // 1. values()でマップの全値を取得する List listValues = new ArrayList(map.values()); // 2. keySet()でマップのキー値をすべて取得する List listKeys = new ArrayList(map.keySet()); // 3. それぞれのリストの内容を確認します。 System.out.println(listValues.toString()); System.out.println(listKeys.toString()); } }
実行結果:
[Warrior, Engineer] [Soldier, Samurai]
map.values()メソッドにてmapの全値のリストを取得しています。
map.keySet()メソッドにてmapの全keyのリストを取得しています。
Streamで変換途中に処理を入れる方法
では、Java8とJava8以前では何が違うのかというと、ラムダ式やstreamを用いて途中操作があれこれできるのがJava8です。
では、Java8以前での方法とjava8での方法、それぞれの処理を見比べてみましょう。
Java8以前の方法
手順としては
- Mapに格納されたすべての要素を取得、
- 各要素のキーと値を取得
という処理になり、ListからMapへの返還の処理と同様になります。
import java.util.HashMap; import java.util.Map; import java.util.Set; public class Main { public static void main(String[] args) { Map map = new HashMap(); map.put("Samurai", "Engineer"); map.put("Soldier", "Warrior"); // 1. Mapに格納されたすべての要素を取得 Set<Map.Entry>set = map.entrySet(); // 2. 各要素のキーと値を取得 for (Map.Entry entry : set) { // 3. ここに操作を書く System.out.println(entry.getKey() + "," + entry.getValue()); } } }
実行結果:
Soldier,Warrior Samurai,Engineer
Java8での方法
ラムダ式でキーと値の繰り返し処理を記述します。
import java.util.HashMap; import java.util.Map; public class Main { public static void main(String[] args) { Map map = new HashMap(); map.put("Samurai", "Engineer"); map.put("Soldier", "Warrior"); map.forEach((key, value) -> System.out.println(key + "," + value)); } }
実行結果:
Soldier,Warrior Samurai,Engineer
同じ処理でもJava8以前に比べ、Java8ではとても簡素ですよね。
Java8ではラムダ式やstreamを用い、操作を連続的に書くことができます。
Java8についてもっと詳しく知りたい方へ
Java8の新機能を使ったListとMapの変換について解説してきました。
Java8のの新機能については、他にも便利な機能があります。
Java8の新機能について
Java8の新機能の詳しい内容については、こちらを参考にしてくださいね!
filter、sorted、mapなどのStrem APIについて
Java8のStream APIにはListやMapなどのコレクションクラスを扱う場合に、filter、sorted、mapなどのAPIがあって便利です。
詳しい内容については、こちらを参考にして下さいね。
まとめ
いかがでしたか?
ListとMapの違いから、java8のラムダ式やStreamについて見てきました。
ラムダ式やStreamを使うと、list⇔Mapの相互変換が簡素に書けるようになります。
みなさんもjava8の恩恵に授かり、一日も早く猛獣、いえ、java使いになられることを祈ります!