ぺんぎんらぼ

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です。たまに絵を描きます。

お笑いとマンガ好きなしょぼしょぼWeb系エンジニアの日記です

Java Tips - Map変換 Collectors.toMapでNullPointerException

みなさん、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を指定できない仕様になっています。

docs.oracle.com

解決方法

では、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のコンディションを忘れないようにしましょう。