本文翻译自:Java 8 NullPointerException in Collectors.toMap
The Java 8 Collectors.toMap
throws a NullPointerException
if one of the values is 'null'. 如果值之一为“ null”,则Java 8 Collectors.toMap
会引发NullPointerException
。 I don't understand this behaviour, maps can contain null pointers as value without any problems. 我不了解这种行为,地图可以包含空指针作为值,而没有任何问题。 Is there a good reason why values cannot be null for Collectors.toMap
? 是否有充分的理由为什么Collectors.toMap
值不能为null?
Also, is there a nice Java 8 way of fixing this, or should I revert to plain old for loop? 另外,是否有解决此问题的不错的Java 8方法,还是应该将其恢复为普通的for循环?
An example of my problem: 我的问题的一个例子:
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));
}
}
Stacktrace: 堆栈跟踪:
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)
This problem still exists in Java 11. Java 11中仍然存在此问题。
#1楼
参考:https://stackoom.com/question/1FlDB/Collectors-toMap中的Java-NullPointerException
#2楼
It is not possible with the static methods of Collectors
. 使用Collectors
的静态方法是不可能的。 The javadoc of toMap
explains that toMap
is based on Map.merge
: toMap
的javadoc解释说toMap
基于Map.merge
:
@param mergeFunction a merge function, used to resolve collisions between values associated with the same key, as supplied to
Map#merge(Object, Object, BiFunction)}
@param mergeFunction一个合并函数,用于解决与相同键关联的值之间的冲突,如提供给Map#merge(Object, Object, BiFunction)}
and the javadoc of Map.merge
says: 并且Map.merge
的javadoc说:
@throws NullPointerException if the specified key is null and this map does not support null keys or the value or remappingFunction is null 如果指定键为null且此映射不支持空键,或者value或remappingFunction 为 null,则@throws NullPointerException
You can avoid the for loop by using the forEach
method of your list. 您可以通过使用列表的forEach
方法来避免for循环。
Map<Integer, Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));
but it is not really simple than the old way: 但这并不比原来的方法简单:
Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
answerMap.put(answer.getId(), answer.getAnswer());
}
#3楼
According to the Stacktrace
根据Stacktrace
Exception in thread "main" java.lang.NullPointerException
at java.util.HashMap.merge(HashMap.java:1216)
at java.util.stream.Collectors.lambda$toMap$148(Collectors.java:1320)
at java.util.stream.Collectors$$Lambda$5/391359742.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 com.guice.Main.main(Main.java:28)
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)
When is called the map.merge
什么时候被称为map.merge
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
It will do a null
check as first thing 它将做一个null
检查作为第一件事
if (value == null)
throw new NullPointerException();
I don't use Java 8 so often so i don't know if there are a better way to fix it, but fix it is a bit hard. 我不经常使用Java 8,所以我不知道是否有更好的方法来修复它,但是修复它有点困难。
You could do: 您可以这样做:
Use filter to filter all NULL values, and in the Javascript code check if the server didn't send any answer for this id means that he didn't reply to it. 使用filter过滤所有NULL值,然后在Javascript代码中检查服务器是否没有为此ID发送任何答案,这意味着他没有答复。
Something like this: 像这样:
Map<Integer, Boolean> answerMap =
answerList
.stream()
.filter((a) -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
Or use peek, which is used to alter the stream element for element. 或使用peek,用于更改元素的流元素。 Using peek you could change the answer to something more acceptable for map but it means edit your logic a bit. 使用peek,您可以将答案更改为地图更可接受的选项,但这意味着需要稍微编辑一下逻辑。
Sounds like if you want to keep the current design you should avoid Collectors.toMap
听起来,如果您想保留当前的设计,则应避免使用Collectors.toMap
#4楼
You can work around this known bug in OpenJDK with this: 您可以使用以下方法来解决OpenJDK中的此已知错误 :
Map<Integer, Boolean> collect = list.stream()
.collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);
It is not that much pretty, but it works. 它不是很漂亮,但是可以工作。 Result: 结果:
1: true
2: true
3: null
( this tutorial helped me the most.) ( 本教程对我的帮助最大。)
#5楼
I wrote a Collector
which, unlike the default java one, does not crash when you have null
values: 我编写了一个Collector
,与默认的Java不同,它在具有null
值时不会崩溃:
public static <T, K, U>
Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> {
Map<K, U> result = new HashMap<>();
for (T item : list) {
K key = keyMapper.apply(item);
if (result.putIfAbsent(key, valueMapper.apply(item)) != null) {
throw new IllegalStateException(String.format("Duplicate key %s", key));
}
}
return result;
});
}
Just replace your Collectors.toMap()
call to a call to this function and it'll fix the problem. 只需将您的Collectors.toMap()
调用替换为对此函数的调用即可,它将解决此问题。
#6楼
Here's somewhat simpler collector than proposed by @EmmanuelTouzery. 这比@EmmanuelTouzery提出的收集器要简单一些。 Use it if you like: 如果喜欢,请使用它:
public static <T, K, U> Collector<T, ?, Map<K, U>> toMapNullFriendly(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper) {
@SuppressWarnings("unchecked")
U none = (U) new Object();
return Collectors.collectingAndThen(
Collectors.<T, K, U> toMap(keyMapper,
valueMapper.andThen(v -> v == null ? none : v)), map -> {
map.replaceAll((k, v) -> v == none ? null : v);
return map;
});
}
We just replace null
with some custom object none
and do the reverse operation in the finisher. 我们只是将null
替换为某些自定义对象none
然后在装订器中执行相反的操作。