Collectors::toMap の値マッピング関数の制約

備忘録。Java でストリームをマップに変換する Collectors::toMapNullPointerException にハマった。


Collectors::toMap はストリームに K 型のキーへマッピングするための関数と V 型の値へマッピングするための関数を受け取って、Map<K, V> 型のマップを返す。

以下は文字列ストリームを文字列とその文字数のマップへ変換する例:

import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

public class Sample {
    public static void main(String[] args) {
        Map<String, Integer> map = Arrays.asList("abc", "def").stream()
            .collect(Collectors.toMap(x -> x, x -> x.length()));
        System.out.println(map); // => {abc=3, def=3}
    }
}

本題は以下のコード。値マッピング関数が null を返している:

import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;

public class Sample {
    public static void main(String[] args) {
        Map<String, Integer> map = Arrays.asList("abc", "def").stream()
            .collect(Collectors.toMap(x -> x, x -> null));
        System.out.println(map);
    }
}

このコードを実行すると toMap の呼び出しの中で NullPointerException が発生する:

Exception in thread "main" java.lang.NullPointerException
        at java.util.HashMap.merge(HashMap.java:1224)
        at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
        at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
        at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
        at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
        at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
        at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
        at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
        at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
        at Sample.main(Sample.java:8)

マップ自体は null を値に取れてほしいが、残念ながら残念ながらこれは仕様上できない。toMap は内部で Map::merge を呼び出す。

そして Map::merge は引数が null の場合に NullPointerException を発生させると明記されている。

どうしても null のような値を埋め込みたい場合は NullObject を導入するか、変換後の値を Optional<V> とするくらいだろう。(マップが Optional を持つというのは少し不自然な気もするが……)