Collectors.toMap 中的 Java 8 NullPointerException
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/24630963/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java 8 NullPointerException in Collectors.toMap
提问by Jasper
The Java 8 Collectors.toMap
throws a NullPointerException
if one of the values is 'null'. 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
?
如果其中一个值为“null”,则Java 8Collectors.toMap
会抛出一个NullPointerException
。我不明白这种行为,地图可以包含空指针作为值而没有任何问题。值不能为空有充分的理由Collectors.toMap
吗?
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$toMap8(Collectors.java:1320)
at java.util.stream.Collectors$$Lambda/1528902577.accept(Unknown Source)
at java.util.stream.ReduceOpsReducingSink.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 中仍然存在。
采纳答案by kajacx
You can work around this known bugin 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
(thistutorial helped me the most.)
(本教程对我帮助最大。)
回答by gontard
It is not possible with the static methods of Collectors
. The javadoc of toMap
explains that toMap
is based on Map.merge
:
的静态方法是不可能的Collectors
。toMap
解释的 javadoctoMap
基于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:
和 javadocMap.merge
说:
@throws NullPointerException if the specified key is null and this map does not support null keys or the valueor remappingFunction isnull
@throws NullPointerException 如果指定的键为空并且此映射不支持空键或值或 remappingFunction为空
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());
}
回答by Marco Acierno
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$toMap8(Collectors.java:1320)
at java.util.stream.Collectors$$Lambda/391359742.accept(Unknown Source)
at java.util.stream.ReduceOpsReducingSink.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. Using peek you could change the answer to something more acceptable for map but it means edit your logic a bit.
或者使用 peek,它用于更改元素的流元素。使用 peek 您可以将答案更改为地图更可接受的答案,但这意味着稍微编辑您的逻辑。
Sounds like if you want to keep the current design you should avoid Collectors.toMap
听起来如果你想保持当前的设计,你应该避免 Collectors.toMap
回答by Emmanuel Touzery
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()
调用替换为对此函数的调用即可解决问题。
回答by Tagir Valeev
Here's somewhat simpler collector than proposed by @EmmanuelTouzery. Use it if you like:
这是比@EmmanuelTouzery 提出的更简单的收集器。如果您喜欢,请使用它:
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
并在完成器中执行相反的操作。
回答by TriCore
NullPointerException is by far the most frequently encountered exception (at least in my case). To avoid this I go defensive and add bunch of null checks and I end up having bloated and ugly code. Java 8 introduces Optional to handle null references so you can define nullable and non-nullable values.
NullPointerException 是迄今为止最常遇到的异常(至少在我的情况下)。为了避免这种情况,我采取防御措施并添加一堆空检查,最终我得到了臃肿和丑陋的代码。Java 8 引入了 Optional 来处理空引用,因此您可以定义可为空和不可为空的值。
That said, I would wrap all the nullable references in Optional container. We should also not break backward compatibility as well. Here is the code.
也就是说,我会将所有可为空的引用包装在 Optional 容器中。我们也不应该破坏向后兼容性。这是代码。
class Answer {
private int id;
private Optional<Boolean> answer;
Answer() {
}
Answer(int id, Boolean answer) {
this.id = id;
this.answer = Optional.ofNullable(answer);
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
/**
* Gets the answer which can be a null value. Use {@link #getAnswerAsOptional()} instead.
*
* @return the answer which can be a null value
*/
public Boolean getAnswer() {
// What should be the default value? If we return null the callers will be at higher risk of having NPE
return answer.orElse(null);
}
/**
* Gets the optional answer.
*
* @return the answer which is contained in {@code Optional}.
*/
public Optional<Boolean> getAnswerAsOptional() {
return answer;
}
/**
* Gets the answer or the supplied default value.
*
* @return the answer or the supplied default value.
*/
public boolean getAnswerOrDefault(boolean defaultValue) {
return answer.orElse(defaultValue);
}
public void setAnswer(Boolean answer) {
this.answer = Optional.ofNullable(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 with optional answers (i.e. with null)
Map<Integer, Optional<Boolean>> answerMapWithOptionals = answerList.stream()
.collect(Collectors.toMap(Answer::getId, Answer::getAnswerAsOptional));
// map in which null values are removed
Map<Integer, Boolean> answerMapWithoutNulls = answerList.stream()
.filter(a -> a.getAnswerAsOptional().isPresent())
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
// map in which null values are treated as false by default
Map<Integer, Boolean> answerMapWithDefaults = answerList.stream()
.collect(Collectors.toMap(a -> a.getId(), a -> a.getAnswerOrDefault(false)));
System.out.println("With Optional: " + answerMapWithOptionals);
System.out.println("Without Nulls: " + answerMapWithoutNulls);
System.out.println("Wit Defaults: " + answerMapWithDefaults);
}
}
回答by Gnana
If the value is a String, then this might work:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
如果该值是一个字符串,那么这可能有效:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
回答by sjngm
Yep, a late answer from me, but I think it may help to understand what's happening under the hood in case anyone wants to code some other Collector
-logic.
是的,我的回答很晚,但我认为如果有人想编写其他Collector
逻辑,了解幕后发生的事情可能会有所帮助。
I tried to solve the problem by coding a more native and straight forward approach. I think it's as direct as possible:
我试图通过编写一种更原生、更直接的方法来解决这个问题。我认为它尽可能直接:
public class LambdaUtilities {
/**
* In contrast to {@link Collectors#toMap(Function, Function)} the result map
* may have null values.
*/
public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) {
return toMapWithNullValues(keyMapper, valueMapper, HashMap::new);
}
/**
* In contrast to {@link Collectors#toMap(Function, Function, BinaryOperator, Supplier)}
* the result map may have null values.
*/
public static <T, K, U, M extends Map<K, U>> Collector<T, M, M> toMapWithNullValues(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper, Supplier<Map<K, U>> supplier) {
return new Collector<T, M, M>() {
@Override
public Supplier<M> supplier() {
return () -> {
@SuppressWarnings("unchecked")
M map = (M) supplier.get();
return map;
};
}
@Override
public BiConsumer<M, T> accumulator() {
return (map, element) -> {
K key = keyMapper.apply(element);
if (map.containsKey(key)) {
throw new IllegalStateException("Duplicate key " + key);
}
map.put(key, valueMapper.apply(element));
};
}
@Override
public BinaryOperator<M> combiner() {
return (left, right) -> {
int total = left.size() + right.size();
left.putAll(right);
if (left.size() < total) {
throw new IllegalStateException("Duplicate key(s)");
}
return left;
};
}
@Override
public Function<M, M> finisher() {
return Function.identity();
}
@Override
public Set<Collector.Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
}
};
}
}
And the tests using JUnit and assertj:
以及使用 JUnit 和 assertj 的测试:
@Test
public void testToMapWithNullValues() throws Exception {
Map<Integer, Integer> result = Stream.of(1, 2, 3)
.collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null));
assertThat(result)
.isExactlyInstanceOf(HashMap.class)
.hasSize(3)
.containsEntry(1, 1)
.containsEntry(2, null)
.containsEntry(3, 3);
}
@Test
public void testToMapWithNullValuesWithSupplier() throws Exception {
Map<Integer, Integer> result = Stream.of(1, 2, 3)
.collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null, LinkedHashMap::new));
assertThat(result)
.isExactlyInstanceOf(LinkedHashMap.class)
.hasSize(3)
.containsEntry(1, 1)
.containsEntry(2, null)
.containsEntry(3, 3);
}
@Test
public void testToMapWithNullValuesDuplicate() throws Exception {
assertThatThrownBy(() -> Stream.of(1, 2, 3, 1)
.collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
.isExactlyInstanceOf(IllegalStateException.class)
.hasMessage("Duplicate key 1");
}
@Test
public void testToMapWithNullValuesParallel() throws Exception {
Map<Integer, Integer> result = Stream.of(1, 2, 3)
.parallel() // this causes .combiner() to be called
.collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null));
assertThat(result)
.isExactlyInstanceOf(HashMap.class)
.hasSize(3)
.containsEntry(1, 1)
.containsEntry(2, null)
.containsEntry(3, 3);
}
@Test
public void testToMapWithNullValuesParallelWithDuplicates() throws Exception {
assertThatThrownBy(() -> Stream.of(1, 2, 3, 1, 2, 3)
.parallel() // this causes .combiner() to be called
.collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
.isExactlyInstanceOf(IllegalStateException.class)
.hasCauseExactlyInstanceOf(IllegalStateException.class)
.hasStackTraceContaining("Duplicate key");
}
And how do you use it? Well, just use it instead of toMap()
like the tests show. This makes the calling code look as clean as possible.
你如何使用它?好吧,只是使用它而不是toMap()
像测试显示的那样。这使调用代码看起来尽可能干净。
EDIT:
implemented Holger's idea below, added a test method
编辑:
在下面实现了 Holger 的想法,添加了一个测试方法
回答by sigirisetti
Retaining all questions ids with small tweak
通过小调整保留所有问题 ID
Map<Integer, Boolean> answerMap =
answerList.stream()
.collect(Collectors.toMap(Answer::getId, a ->
Boolean.TRUE.equals(a.getAnswer())));
回答by Luca
Sorry to reopen an old question, but since it was edited recently saying that the "issue" still remains in Java 11, I felt like I wanted to point out this:
很抱歉重新打开一个旧问题,但由于最近对其进行了编辑,说“问题”仍然存在于 Java 11 中,我觉得我想指出这一点:
answerList
.stream()
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
gives you the null pointer exception because the map does not allow null as a value.
This makes sense because if you look in a map for the key k
and it is not present, then the returned value is already null
(see javadoc). So if you were able to put in k
the value null
, the map would look like it's behaving oddly.
为您提供空指针异常,因为该映射不允许将 null 作为值。这是有道理的,因为如果您在映射中查找键k
并且它不存在,那么返回的值已经存在null
(请参阅 javadoc)。因此,如果您能够k
输入 value null
,则地图看起来会很奇怪。
As someone said in the comments, it's pretty easy to solve this by using filtering:
正如有人在评论中所说,使用过滤很容易解决这个问题:
answerList
.stream()
.filter(a -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
in this way no null
values will be inserted in the map, and STILL you will get null
as the "value" when looking for an id that does not have an answer in the map.
这样null
,地图中不会插入任何值,并且null
在查找地图中没有答案的 id 时,您仍然会得到“值”。
I hope this makes sense to everyone.
我希望这对每个人都有意义。