Collectors.toMap中空条目值导致NullPointerException

539

Collectors.toMap方法会在值为null时抛出NullPointerException异常。我不理解这种行为,因为map可以包含值为null的指针并且没有任何问题。是否有充分的理由使得Collectors.toMap不支持值为null的情况?

此外,是否有Java 8中优雅的方式来解决这个问题,或者我应该回归到普通的for循环?

以下是我的问题示例:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;


class Answer {
    private int id;

    private Boolean answer;

    Answer() {
    }

    Answer(int id, Boolean answer) {
        this.id = id;
        this.answer = answer;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public Boolean getAnswer() {
        return answer;
    }

    public void setAnswer(Boolean answer) {
        this.answer = answer;
    }
}

public class Main {
    public static void main(String[] args) {
        List<Answer> answerList = new ArrayList<>();

        answerList.add(new Answer(1, true));
        answerList.add(new Answer(2, true));
        answerList.add(new Answer(3, null));

        Map<Integer, Boolean> answerMap =
        answerList
                .stream()
                .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
    }
}

堆栈跟踪:

Exception in thread "main" java.lang.NullPointerException
    at java.util.HashMap.merge(HashMap.java:1216)
    at java.util.stream.Collectors.lambda$toMap$168(Collectors.java:1320)
    at java.util.stream.Collectors$$Lambda$5/1528902577.accept(Unknown Source)
    at java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    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 Main.main(Main.java:48)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

这个问题在Java 11中仍然存在。


6
null 一直存在问题,就像在 TreeMap 中一样。也许现在是尝试使用 Optional<Boolean> 的好时机?否则可以进行分割并使用过滤器。 - Joop Eggen
17
“null”可能是键的问题,但在这种情况下,它是值。 - gontard
3
并非所有的映射都会出现null的问题,例如 HashMap 可以有一个 null 键和任意数量的 null 值。您可以尝试使用 HashMap 创建自定义 Collector,而不是使用默认的 Collector - kajacx
6
但默认实现是HashMap - 如堆栈跟踪的第一行所示。问题不在于Map不能容纳null值,而在于Map#merge函数的第二个参数不能为null。 - czerny
1
就个人而言,在给定的情况下,如果输入是并行的,我会选择非流式解决方案或forEach()。下面这些简洁的基于流的解决方案可能会有可怕的性能问题。 - Ondra Žižka
显示剩余2条评论
13个回答

2
public static <T, K, V> Collector<T, HashMap<K, V>, HashMap<K, V>> toHashMap(
        Function<? super T, ? extends K> keyMapper,
        Function<? super T, ? extends V> valueMapper
)
{
    return Collector.of(
            HashMap::new,
            (map, t) -> map.put(keyMapper.apply(t), valueMapper.apply(t)),
            (map1, map2) -> {
                map1.putAll(map2);
                return map1;
            }
    );
}

public static <T, K> Collector<T, HashMap<K, T>, HashMap<K, T>> toHashMap(
        Function<? super T, ? extends K> keyMapper
)
{
    return toHashMap(keyMapper, Function.identity());
}

1
点赞因为这段代码可以编译。被接受的答案无法编译,因为Map::putAll没有返回值。 - Taugenichts

2

为了完整起见,我发布了一个带有mergeFunction参数的toMapOfNullables版本:

public static <T, K, U> Collector<T, ?, Map<K, U>> toMapOfNullables(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, BinaryOperator<U> mergeFunction) {
    return Collectors.collectingAndThen(Collectors.toList(), list -> {
        Map<K, U> result = new HashMap<>();
        for(T item : list) {
            K key = keyMapper.apply(item);
            U newValue = valueMapper.apply(item);
            U value = result.containsKey(key) ? mergeFunction.apply(result.get(key), newValue) : newValue;
            result.put(key, value);
        }
        return result;
    });
}

1

只需将可空值包装为Optional。非常优雅的解决方法。

    listOfValues.stream()
    .collect(Collectors.toMap(e -> e.getKey(), e -> 
    Optional.ofNullable(e.getValue())))

2
那么,当它应该为null时,该值是可选的。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接