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
?
Also, is there a nice Java 8 way of fixing this, or should I revert to plain old for loop?
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.
It is not possible with the static methods of
Collectors
. The javadoc oftoMap
explains thattoMap
is based onMap.merge
:and the javadoc of
Map.merge
says:You can avoid the for loop by using the
forEach
method of your list.but it is not really simple than the old way:
Here's somewhat simpler collector than proposed by @EmmanuelTouzery. Use it if you like:
We just replace
null
with some custom objectnone
and do the reverse operation in the finisher.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:
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 alreadynull
(see javadoc). So if you were able to put ink
the valuenull
, the map would look like it's behaving oddly.As someone said in the comments, it's pretty easy to solve this by using filtering:
in this way no
null
values will be inserted in the map, and STILL you will getnull
as the "value" when looking for an id that does not have an answer in the map.I hope this makes sense to everyone.
According to the
Stacktrace
When is called the
map.merge
It will do a
null
check as first thingI 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.
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.
Something like this:
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.
Sounds like if you want to keep the current design you should avoid
Collectors.toMap
I wrote a
Collector
which, unlike the default java one, does not crash when you havenull
values:Just replace your
Collectors.toMap()
call to a call to this function and it'll fix the problem.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.I tried to solve the problem by coding a more native and straight forward approach. I think it's as direct as possible:
And the tests using JUnit and assertj:
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.