Stream流中Collectors.toMap方法内value为null导致空指针异常问题

Stream流中Collectors.toMap方法内value为null导致空指针异常问题

LonelyMan 240 2023-11-09

起因

编写完一个需求后在联调时出现了一个NPE异常,日志如下

java.lang.NullPointerException: null
	at java.util.HashMap.merge(HashMap.java:1226)
	at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1320)
	at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.util.Collections$2.tryAdvance(Collections.java:4719)
	at java.util.Collections$2.forEachRemaining(Collections.java:4727)
	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:566)
	at com.xxx.xxx.service.impl.DynamicFormColumnServiceImpl.lambda$getCodePropertyValue$24(DynamicFormColumnServiceImpl.java:465)
	at java.util.stream.Collectors.lambda$toMap$58(Collectors.java:1321)
	at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
	at java.util.HashMap$EntrySpliterator.forEachRemaining(HashMap.java:1723)
	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:566)
	at com.xxx.xxx.service.impl.DynamicFormColumnServiceImpl.getCodePropertyValue(DynamicFormColumnServiceImpl.java:463)

对应出错的代码如下

sourceDataList.stream().collect(Collectors.toMap(
    sourceData -> sourceData,
    sourceData -> CommonUtil.convertType(
        ReflectUtil.getFieldValue(sourceData, entry.getValue().getRowName()),
        entry.getKey().getValueType().getClazz()
    )
)

已知sourceDataList不为空,且其中也没有为null的元素,并且CommonUtil.convertType的流程不会抛出异常

分析

根据以上信息分析可以知道,问题就发生在CommonUtil.convertType的返回结果上

但是根据以往经验,HashMap是允许一个keynull或多个valuenull的,所以应该是其他的地方没有注意到

查看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, element) -> map.merge(keyMapper.apply(element),
                                      valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

发现在Collectors.toMap的调用过程中并不是我们平常常用的put方法,而是merge

查看HashMap.merge方法源码,内容如下

@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();
    int hash = hash(key);
    .................
    return value;
}

其中明确写到如果value为空则抛出NPE异常

向上查看Map接口中的merge定义,内容如下

default V merge(K key, V value,
                BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
    remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}

可以看到判断方式也与HashMap中重写的方法类似

解决

  1. 换用.forEach方法

    Map<T, Object> map = new HashMap<>();
    sourceDataList.forEach(
        sourceData -> map.put(
            sourceData,
            CommonUtil.convertType(
                ReflectUtil.getFieldValue(sourceData, entry.getValue().getRowName()),
                entry.getKey().getValueType().getClazz()
            )
        )
    );
    return map;
    
  2. 写一个自定义的Collector(推荐)

    sourceDataList.stream()
        .collect(Collector.of(
            HashMap::new,
            (map, sourceData) ->
            map.put(
                sourceData,
                CommonUtil.convertType(
                    ReflectUtil.getFieldValue(sourceData, entry.getValue().getRowName()),
                    entry.getKey().getValueType().getClazz()
                )
            ),
            (o1, o2) -> o1
        ))
    
    

    参考

    Collectors.toMap不允许Null Value导致NPE

    鸣谢

    @黄同学——向黄同学学习!