みなさん、Stream API使ってますか?
Java8で導入されたStream APIですが、メソッドが多くて、なかなかすべてを覚えられません。
今回は、Streamの結果をMapに変換する方法のお話です。
Collectors.toMap
Stream APIでMap変換を実装しようとすると、最初に思いつくのは終端操作のcollectでCollectors.toMapを使用する方法です。
以下は、ListをMapに変換するサンプルです。
List<String> list = Arrays.asList("penguin", "matsuki", "java"); Map<Integer, String> map = IntStream.range(0, list.size()) .boxed() .collect(Collectors.toMap(i -> i, list::get)); System.out.println(map);
非常にシンプルです。サンプルは見やすくするために改行を入れていますが、ワンライナーで書けるのも魅力です。
実行結果は次のようになります。
{0=penguin, 1=matsuki, 2=java}
しかし、このソースには問題があるんです。
Listの要素にnullを含めてみます。
List<String> list = Arrays.asList("penguin", "matsuki", "java", null);
Mapの値(Value)はnullを含めることができるので、問題なく動きそうですが、実行すると、NullPointerExceptionが発生します。
toMapの内部では、Map.mergeメソッドを使用しています。
private static <K, V, M extends Map<K,V>> BinaryOperator<M> mapMerger(BinaryOperator<V> mergeFunction) { return (m1, m2) -> { for (Map.Entry<K,V> e : m2.entrySet()) m1.merge(e.getKey(), e.getValue(), mergeFunction); return m1; }; }
そして、このmergeメソッドは、値にnullを指定できない仕様になっています。
解決方法
では、Stream APIで値にnullを含めるMapを作れないかというと、そうではなく、別の方法でMapを作成できます。
List<String> list = Arrays.asList("penguin", "matsuki", "java", null); Map<Integer, String> map = IntStream.range(0, list.size()) .boxed() .collect(HashMap::new, (m, i) -> m.put(i, list.get(i)), HashMap::putAll); System.out.println(map);
collectの引数が変わりました。
第1引数
第1引数には結果のオブジェクトを生成する関数を指定します。
今回は、結果をMapで返したいので、HashMapのインスタンスを生成(new)する式を記述します。
HashMap::new
第2引数
第2引数には結果のオブジェクトに1件のデータを格納する関数を指定します。
今回は、Mapに1件のデータを設定(put)する式を記述します。
(m, i) -> m.put(i, list.get(i))
第3引数
第3引数には結果のオブジェクト同士を1つのオブジェクトにまとめる関数を指定します。第1引数で1件のデータを格納するオブジェクトを生成して、第2引数で1件のデータを格納しています。つまり、件数ごとにHashMapが生成されたことになります。それを1つのオブジェクトにまとめる必要があるので、その方法を第3引数に指定するのです。
今回は、Mapをまとめる(putAll)ための式を記述します。
HashMap::putAll
まとめ
Stream APIでListを生成するときは、Collectors.toListを使います。それと同じノリで、Mapを生成するときにCollectors.toMapを使うと、思わぬバグを生みます。
テストのときにMapの値がnullになるケースを忘れると、バグに気付かないことがあるため、テストケースでは値がnullのコンディションを忘れないようにしましょう。