NullPointerException in Collectors.toMap with null entry values

Collectors.toMap抛出一个 if,其中一个值为 。我不理解这种行为,地图可以包含空指针作为值,没有任何问题。有没有充分的理由说明为什么值不能为空?NullPointerExceptionnullCollectors.toMap

另外,有没有一个很好的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 中仍然存在。


答案 1

你可以在 OpenJDK 中解决这个已知的 bug

Map<Integer, Boolean> collect = list.stream()
        .collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);

它不是那么漂亮,但它有效。结果:

1: true
2: true
3: null

(本教程对我帮助最大。

编辑:

与 不同,如果您多次使用相同的键,这将静默地替换值,如注释中@mmdemirbas指出的那样。如果您不想要这个,请查看评论中的链接。Collectors.toMap


答案 2

使用 的静态方法无法做到这一点。toMap的javadoc解释说,这是基于Map.merge的CollectorstoMap

@param mergeFunction 是一个合并函数,用于解决与同一键关联的值之间的冲突,提供给Map#merge(Object, Object, BiFunction)}

Map.merge的javadoc说:

@throws NullPointerException 如果指定的键为 null 并且此映射不支持 null 键,或者或重新映射函数 null

您可以通过使用列表的 forEach 方法来避免 for 循环。

Map<Integer,  Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));

但它并不比旧方法简单:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}

推荐