最近线上偶尔会报一个 NPE,是Collectors.toMap
导致的,这里小记一下,防止再次踩坑。
场景:批量查询用户信息,查询结果为List<User>
,然后将其转换成Map<Integer, String>
,以供其他地方使用,但在Collectors.toMap
时抛出了异常NullPointerException
。
复现问题
publicclassToMapTest{publicstaticvoidmain(String[] args){List<User> users=query();Map<Integer,String> userMap= users.stream().collect(Collectors.toMap(User::getId,User::getGirlfriend));}publicstaticList<User>query(){List<User> list=newArrayList<>();// girlfriend 为 null
list.add(newUser(1,null));return list;}}@DataclassUser{privatefinalInteger id;privatefinalString girlfriend;}
当User
的 girlfriend 为 null 时,会抛出 NPE(单身狗落泪!!!)
Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1225)
at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1384)
at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482)
at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:472)
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 com.example.collectors.ToMapTest.main(ToMapTest.java:19)
原因
在第 16 行,会调用map.merge
,且两个参数的toMap
方法默认是创建HashMap
(第 5 行)。
// Collectors.toMappublicstatic<T,K,U>Collector<T,?,Map<K,U>>toMap(Function<?superT,?extendsK> keyMapper,Function<?superT,?extendsU> valueMapper){returntoMap(keyMapper, valueMapper,throwingMerger(),HashMap::new);}// Collectors.toMappublicstatic<T,K,U,MextendsMap<K,U>>Collector<T,?,M>toMap(Function<?superT,?extendsK> keyMapper,Function<?superT,?extendsU> valueMapper,BinaryOperator<U> mergeFunction,Supplier<M> mapSupplier){BiConsumer<M,T> accumulator// 调用 map.merge 方法=(map, element)-> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);returnnewCollectorImpl<>(mapSupplier, accumulator,mapMerger(mergeFunction), CH_ID);}
原因很简单,第 4 行判断了value == null
,所以抛出了NullPointerException
。
@OverridepublicVmerge(K key,V value,BiFunction<?superV,?superV,?extendsV> remappingFunction){if(value==null)thrownewNullPointerException();if(remappingFunction==null)thrownewNullPointerException();......}
解决办法
以下两种方法与Collectors.toMap
方法的区别是:当出现重复的 key 时,Collectors.toMap
默认会抛异常,而以下两种方式会用新值覆盖旧值。
// Collectors.toMap 默认抛异常privatestatic<T>BinaryOperator<T>throwingMerger(){return(u,v)->{thrownewIllegalStateException(String.format("Duplicate key %s", u));};}
Stream.collect 方法
Stream
的collect
可以实现简易的Collector
功能:
// Stream.collect()<R>Rcollect(Supplier<R> supplier,BiConsumer<R,?superT> accumulator,BiConsumer<R,R> combiner);publicstaticvoidmain(String[] args){List<User> users=query();Map<Integer,String> userMap2= users.stream().collect(HashMap::new,(map, user)-> map.put(user.getId(), user.getGirlfriend()),HashMap::putAll);System.out.println(userMap2);}// 输出// {1=null}
直接使用 Collector
与使用Stream.collect
方法相比,Collector
多了一个Characteristics
参数,这里用不到所以给了空数组:
publicstaticvoidmain(String[] args){List<User> users=query();Collector<User,Map<Integer,String>,Map<Integer,String>> collector=Collector.of(HashMap::new,(map, user)-> map.put(user.getId(), user.getGirlfriend()),(map1, map2)->{
map1.putAll(map2);return map1;},newCollector.Characteristics[]{});Map<Integer,String> userMap2= users.stream().collect(collector);System.out.println(userMap2);}// 输出// {1=null}