最近线上偶尔会报一个 NPE,是 Collectors.toMap
导致的,这里小记一下,防止再次踩坑。
场景:批量查询用户信息,查询结果为 List
,然后将其转换成 Map
,以供其他地方使用,但在 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
可以实现简易的 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}
与使用 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}