備忘録。Java でストリームをマップに変換する Collectors::toMap
の NullPointerException
にハマった。
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
を持つというのは少し不自然な気もするが……)