Java 8 Collectors.toMap
arroja un NullPointerException
si uno de los valores es 'nulo'. No entiendo este comportamiento, los mapas pueden contener punteros nulos como valor sin ningún problema. ¿Hay una buena razón por la cual los valores no pueden ser nulos Collectors.toMap
?
Además, ¿hay una buena forma de Java 8 para solucionar esto, o debería volver al ciclo anterior para el bucle?
Un ejemplo de mi 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)
Este problema todavía existe en Java 11.
null
siempre fue un poco problemático, como en TreeMap. Tal vez un buen momento para probarOptional<Boolean>
? De lo contrario, divida y use el filtro.null
podría ser un problema para una clave, pero en este caso es el valor.null
,HashMap
por ejemplo, pueden tener unanull
clave y cualquier número denull
valores, puede intentar crear una personalizadaCollector
utilizando una enHashMap
lugar de utilizar la predeterminada.HashMap
, como se muestra en la primera línea de stacktrace. El problema no es que un valorMap
no puede contenernull
, sino que el segundo argumento de laMap#merge
función no puede ser nulo.Respuestas:
Puede solucionar este error conocido en OpenJDK con esto:
No es tan bonito, pero funciona. Resultado:
( Este tutorial me ayudó más).
fuente
() -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)
crear unaString
clave insensible a mayúsculas y 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);
. Tuve: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
y luego llamaputAll()
para cada entrada individual. Personalmente, en determinadas circunstancias, elegiría una solución que no sea de flujo, oforEach()
si la entrada es paralela.No es posible con los métodos estáticos de
Collectors
. El javadoc detoMap
explica quetoMap
se basa enMap.merge
:y el javadoc de
Map.merge
dice:Puede evitar el bucle for utilizando el
forEach
método de su lista.pero no es realmente simple que a la antigua usanza:
fuente
Map.merge
. Este en mi humilde opinión es un defecto en la implementación que restringe un caso de uso perfectamente aceptable que se ha pasado por alto. Los métodos sobrecargados detoMap
sí indican el uso deMap.merge
OP, pero no el que está utilizando.Escribí una
Collector
que, a diferencia de la predeterminada de Java, no se bloquea cuando tienesnull
valores:Simplemente reemplace su
Collectors.toMap()
llamada por una llamada a esta función y solucionará el problema.fuente
null
valores y usarputIfAbsent
no juega bien juntos. No detecta claves duplicadas cuando se asignan anull
...Sí, una respuesta tardía de mi parte, pero creo que puede ser útil entender lo que sucede debajo del capó en caso de que alguien quiera codificar alguna otra
Collector
lógica.Traté de resolver el problema codificando un enfoque más nativo y directo. Creo que es lo más directo posible:
Y las pruebas usando JUnit y afirm:
y como lo usas? Bueno, solo úsalo en lugar de
toMap()
como lo muestran las pruebas. Esto hace que el código de llamada se vea lo más limpio posible.EDITAR:
implementó la idea de Holger a continuación, agregó un método de prueba
fuente
(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()
realidad lo comprueba. Tal vez debería hacer algunas transmisiones paralelas una vez :)Aquí hay un recopilador algo más simple que el propuesto por @EmmanuelTouzery. Úselo si le gusta:
Simplemente lo reemplazamos
null
con algún objeto personalizadonone
y hacemos la operación inversa en el finalizador.fuente
Si el valor es una Cadena, esto podría funcionar:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
fuente
De acuerdo con la
Stacktrace
Cuando se llama el
map.merge
Hará un
null
chequeo como primera cosaNo uso Java 8 con tanta frecuencia, así que no sé si hay una mejor manera de solucionarlo, pero solucionarlo es un poco difícil.
Podrías hacerlo:
Use filter para filtrar todos los valores NULL, y en el código Javascript verifique si el servidor no envió ninguna respuesta para esta identificación, lo que significa que no respondió.
Algo como esto:
O use peek, que se usa para alterar el elemento stream por elemento. Usando peek, podría cambiar la respuesta a algo más aceptable para el mapa, pero significa editar su lógica un poco.
Parece que si desea mantener el diseño actual, debe evitar
Collectors.toMap
fuente
He modificado ligeramente la implementación de Emmanuel Touzery .
Esta versión;
Pruebas unitarias:
fuente
Lamento volver a abrir una pregunta anterior, pero como se editó recientemente diciendo que el "problema" aún permanece en Java 11, sentí que quería señalar esto:
le da la excepción de puntero nulo porque el mapa no permite un valor nulo. Esto tiene sentido porque si busca en un mapa la clave
k
y no está presente, entonces el valor devuelto ya estánull
(ver javadoc). Entonces, si pudieras ponerk
el valornull
, el mapa parecería que se comporta de manera extraña.Como alguien dijo en los comentarios, es bastante fácil resolver esto mediante el filtrado:
de esta manera no
null
se insertarán valores en el mapa, y TODAVÍA se obtendránull
como el "valor" cuando se busca una identificación que no tiene una respuesta en el mapa.Espero que esto tenga sentido para todos.
fuente
answerMap.put(4, null);
sin ningún problema. Tiene razón en que con su solución propuesta obtendrá el mismo resultado para anserMap.get () si no está presente como si el valor se insertara como nulo. Sin embargo, si itera sobre todas las entradas del mapa, obviamente hay una diferencia.fuente
Retener todas las preguntas con identificadores pequeños
fuente
NullPointerException es, con mucho, la excepción más frecuente (al menos en mi caso). Para evitar esto, me pongo a la defensiva y agrego un montón de controles nulos y termino teniendo un código hinchado y feo. Java 8 presenta Opcional para manejar referencias nulas para que pueda definir valores anulables y no anulables.
Dicho esto, envolvería todas las referencias anulables en el contenedor opcional. Tampoco debemos romper la compatibilidad con versiones anteriores. Aquí está el código.
fuente
Collectors.toMap()
valores no nulos