最近线上偶尔会报一个 NPE,是 Collectors.toMap 导致的,这里小记一下,防止再次踩坑。
场景:批量查询用户信息,查询结果为 List<User>
,然后将其转换成 Map<Integer, String>
,以供其他地方使用,但在 Collectors.toMap 时抛出了异常 NullPointerException
。
public class ToMapTest {
public static void main(String[] args) {
List<User> users = query();
Map<Integer, String> userMap = users.stream()
.collect(Collectors.toMap(User::getId, User::getGirlfriend));
}
public static List<User> query() {
List<User> list = new ArrayList<>();
// girlfriend 为 null
list.add(new User(1, null));
return list;
}
}
@Data
class User {
private final Integer id;
private final String 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.toMap
public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new);
}
// Collectors.toMap
public static <T, K, U, M extends Map<K, U>>
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction,
Supplier<M> mapSupplier) {
BiConsumer<M, T> accumulator
// 调用 map.merge 方法
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}
原因很简单,第 4 行判断了 value == null,所以抛出了 NullPointerException。
@Override
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
......
}
以下两种方法与 Collectors.toMap 方法的区别是:当出现重复的 key 时, Collectors.toMap 默认会抛异常,而以下两种方式会用新值覆盖旧值。
// Collectors.toMap 默认抛异常
private static <T> BinaryOperator<T> throwingMerger() {
return (u,v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); };
}
Stream.collect 方法
Stream 的 collect 可以实现简易的 Collector 功能:
// Stream.collect()
<R> R collect(Supplier<R> supplier,
BiConsumer<R, ? super T> accumulator,
BiConsumer<R, R> combiner);
public static void main(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 参数,这里用不到所以给了空数组:
public static void main(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;
},
new Collector.Characteristics[]{}
);
Map<Integer, String> userMap2 = users.stream()
.collect(collector);
System.out.println(userMap2);
}
// 输出
// {1=null}
鸣谢:https://blog.csdn.net/qq_20919883/article/details/119654002