O Java 8 Collectors.toMap
lança a NullPointerException
se um dos valores for 'nulo'. Eu não entendo esse comportamento, os mapas podem conter ponteiros nulos como valor sem problemas. Existe uma boa razão pela qual os valores não podem ser nulos Collectors.toMap
?
Além disso, existe uma maneira agradável do Java 8 de corrigir isso, ou devo reverter para o antigo loop for?
Um exemplo do meu problema:
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)
Esse problema ainda existe no Java 11.
null
sempre foi um pouco problemático, como no TreeMap. Talvez um bom momento para experimentarOptional<Boolean>
? Caso contrário, divida e use o filtro.null
pode ser um problema para uma chave, mas, neste caso, é o valor.null
,HashMap
por exemplo, podem ter umanull
chave e qualquer número denull
valores, você pode tentar criar um personalizadoCollector
usando a emHashMap
vez de usar o padrão.HashMap
- como mostrado na primeira linha do stacktrace. O problema não é que um valorMap
não pode conternull
, mas que o segundo argumento daMap#merge
função não pode ser nulo.Respostas:
Você pode contornar isso bug conhecido no OpenJDK com isso:
Não é tão bonito, mas funciona. Resultado:
( este tutorial me ajudou mais.)
fonte
() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)
criar umaString
chave sem distinção entre maiúsculas e minúsculasTreeMap
.Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);
. Eu tinha:incompatible types: cannot infer type-variable(s) R (argument mismatch; invalid method reference no suitable method found for putAll(java.util.Map<java.lang.Integer,java.lang.Boolean>,java.util.Map<java.lang.Integer,java.lang.Boolean>) method java.util.Map.putAll(java.util.Map) is not applicable (actual and formal argument lists differ in length)
HashMap
e depois chamaputAll()
para cada entrada. Pessoalmente, em determinadas circunstâncias, eu usaria uma solução sem fluxo ouforEach()
se a entrada for paralela.Não é possível com os métodos estáticos de
Collectors
. O javadoc detoMap
explica quetoMap
é baseado emMap.merge
:e o javadoc de
Map.merge
diz:Você pode evitar o loop for usando o
forEach
método da sua lista.mas não é realmente simples do que o jeito antigo:
fonte
Map.merge
. Esse IMHO é uma falha na implementação que restringe um caso de uso perfeitamente aceitável que foi ignorado. Os métodos sobrecarregados detoMap
afirmam o uso,Map.merge
mas não o que o OP está usando.Eu escrevi um
Collector
que, diferente do java padrão, não falha quando você temnull
valores:Basta substituir sua
Collectors.toMap()
chamada por uma chamada para esta função e isso resolverá o problema.fonte
null
valores e usarputIfAbsent
não funciona bem juntos. Ele não detecta chaves duplicadas quando eles são mapeados paranull
...Sim, uma resposta tardia minha, mas acho que pode ajudar a entender o que está acontecendo sob o capô, caso alguém queira codificar alguma outra
Collector
lógica.Tentei resolver o problema codificando uma abordagem mais nativa e direta. Eu acho que é o mais direto possível:
E os testes usando JUnit e assertj:
E como você usa isso? Bem, basta usá-lo em vez de
toMap()
como mostram os testes. Isso faz com que o código de chamada pareça o mais limpo possível.EDIT:
implementou a ideia de Holger abaixo, adicionou um método de teste
fonte
(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }
accumulator()
realmente verifica isso. Talvez eu devesse fazer algumas correntes paralelas, uma vez :)Aqui está um colecionador um pouco mais simples do que o proposto por @EmmanuelTouzery. Use-o se quiser:
Apenas substituímos
null
por algum objeto personalizadonone
e fazemos a operação inversa no finalizador.fonte
Se o valor for uma String, isso poderá funcionar:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
fonte
De acordo com
Stacktrace
Quando é chamado de
map.merge
Ele fará uma
null
verificação como primeira coisaEu não uso o Java 8 com tanta frequência, então não sei se há uma maneira melhor de corrigi-lo, mas corrigi-lo é um pouco difícil.
Você poderia fazer:
Use filter para filtrar todos os valores NULL e, no código Javascript, verifique se o servidor não enviou nenhuma resposta para esse ID significa que ele não respondeu.
Algo assim:
Ou use peek, que é usado para alterar o elemento de fluxo para o elemento. Usando o peek, você pode alterar a resposta para algo mais aceitável para o mapa, mas isso significa editar um pouco sua lógica.
Parece que, se você quiser manter o design atual, evite
Collectors.toMap
fonte
Eu ligeiramente modificada implementação do Emmanuel Touzery .
Esta versão;
Testes unitários:
fonte
Desculpe reabrir uma pergunta antiga, mas desde que ela foi editada recentemente dizendo que o "problema" ainda permanece no Java 11, senti que queria apontar isso:
fornece a exceção do ponteiro nulo porque o mapa não permite nulo como um valor. Isso faz sentido, porque se você procurar a chave em um mapa
k
e ela não estiver presente, o valor retornado já estaránull
(consulte javadoc). Portanto, se você pudesse colocark
o valornull
, o mapa pareceria estar se comportando de maneira estranha.Como alguém disse nos comentários, é muito fácil resolver isso usando a filtragem:
dessa forma, nenhum
null
valor será inserido no mapa e AINDA você obteránull
o "valor" ao procurar um ID que não tenha resposta no mapa.Espero que isso faça sentido para todos.
fonte
answerMap.put(4, null);
sem problemas. Você está certo que, com a solução proposta, obterá o mesmo resultado para anserMap.get () se ele não estiver presente, como se o valor fosse inserido como nulo. No entanto, se você iterar sobre todas as entradas do mapa, obviamente haverá uma diferença.fonte
Reter todos os IDs de perguntas com pequenos ajustes
fonte
NullPointerException é de longe a exceção mais frequentemente encontrada (pelo menos no meu caso). Para evitar isso, fico na defensiva e adiciono várias verificações nulas e acabo tendo um código inchado e feio. O Java 8 introduz o Opcional para manipular referências nulas, para que você possa definir valores nulos e não nulos.
Dito isto, gostaria de agrupar todas as referências anuláveis no contêiner opcional. Também não devemos quebrar a compatibilidade com versões anteriores. Aqui está o código.
fonte
Collectors.toMap()
valores não nulos