nulls - Java 8 NullPointerException en Collectors.toMap
java 8 stream map ignore null (10)
Aquí hay un coleccionista algo más simple que el propuesto por @EmmanuelTouzery. Úsalo si te gusta:
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;
});
}
Simplemente reemplazamos null
con algún objeto personalizado none
y hacemos la operación inversa en el finalizador.
Java 8 Collectors.toMap
lanza una 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 que los valores no pueden ser nulos para Collectors.toMap
?
Además, ¿existe una buena forma de arreglar esto de Java 8, o debo volver al simple 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.
Escribí un Collector
que, a diferencia del java predeterminado, no falla cuando tienes valores 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;
});
}
Simplemente reemplace su llamada de Collectors.toMap()
a una llamada a esta función y solucionará el problema.
La excepción 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 cheques nulos y termino con un código hinchado y feo. Java 8 introduce Opcional para manejar las referencias nulas para que pueda definir valores que puedan contener nulos y que no puedan ser nulos.
Dicho esto, envolvería todas las referencias anulables en un contenedor opcional. Tampoco debemos romper la compatibilidad hacia atrás también. Aquí está el código.
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);
}
}
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:
answerList
.stream()
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
le da la excepción de puntero nulo porque el mapa no permite nulo como valor. Esto tiene sentido porque si busca en el mapa la clave k
y no está presente, entonces el valor devuelto ya es null
(vea javadoc). Entonces, si pudieras poner en k
el valor null
, el mapa parecería que se está comportando de manera extraña.
Como alguien dijo en los comentarios, es bastante fácil de resolver esto mediante el filtrado:
answerList
.stream()
.filter(a -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
de esta manera, no se insertarán valores null
en el mapa, y TODAVÍA obtendrá un null
como "valor" cuando busque un ID que no tenga una respuesta en el mapa.
Espero que esto tenga sentido para todos.
No es posible con los métodos estáticos de los Collectors
. El javadoc de toMap
explica que toMap
se basa en Map.merge
:
@param mergeFunction una función de combinación, utilizada para resolver colisiones entre valores asociados con la misma clave, como se suministra a
Map#merge(Object, Object, BiFunction)}
y el javadoc de Map.merge
dice:
@ throws NullPointerException si la clave especificada es nula y este mapa no admite claves nulas o el valor or remappingFunction es nulo
Puede evitar el bucle for utilizando el método forEach
de su lista.
Map<Integer, Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));
Pero en realidad no es tan simple como la forma antigua:
Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
answerMap.put(answer.getId(), answer.getAnswer());
}
Puedes solucionar este bugs.openjdk.java.net/browse/JDK-8148463 en OpenJDK con esto:
Map<Integer, Boolean> collect = list.stream()
.collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);
No es muy bonito, pero funciona. Resultado:
1: true
2: true
3: null
( this tutorial me ayudó más.)
Reteniendo todas las preguntas con id pequeños tweak
Map<Integer, Boolean> answerMap = answerList.stream().collect(Collectors.toMap(Answer::getId, a -> Boolean.TRUE.equals(a.getAnswer())));
Según el 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)
Cuando se llama el map.merge
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
Hará un cheque null
como primera cosa.
if (value == null)
throw new NullPointerException();
No uso Java 8 tan a menudo, así que no sé si hay una mejor manera de solucionarlo, pero arreglarlo es un poco difícil.
Podrías hacerlo:
Use el filtro para filtrar todos los valores NULOS, y en el código de Javascript compruebe si el servidor no envió ninguna respuesta para esta identificación significa que no respondió a ella.
Algo como esto:
Map<Integer, Boolean> answerMap =
answerList
.stream()
.filter((a) -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
O use peek, que se usa para alterar el elemento de flujo para el elemento. Usando Peek puedes cambiar la respuesta a algo más aceptable para el mapa, pero esto significa editar un poco tu lógica.
Parece que si desea mantener el diseño actual, debe evitar Collectors.toMap
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("")))
Sí, una respuesta tardía de mi parte, pero creo que puede ayudar a entender lo que está pasando debajo del capó en caso de que alguien quiera codificar algún otro Collector
lógico.
Intenté resolver el problema codificando un enfoque más nativo y directo. Creo que es lo más directo posible:
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 (map1, map2) -> {
map1.putAll(map2);
return map1;
};
}
@Override
public Function<M, M> finisher() {
return Function.identity();
}
@Override
public Set<Collector.Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
}
};
}
}
Y las pruebas utilizando JUnit y 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);
}
y como lo usas? Bueno, solo toMap()
en lugar de toMap()
como muestran las pruebas. Esto hace que el código de llamada se vea lo más limpio posible.