Collectors.toMap 报错 NullPointerException

最近线上偶尔会报一个 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 方法

Streamcollect 可以实现简易的 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}

你可能感兴趣的:(java,踩坑,Collectors,toMap,java,stream,NPE)