Java 8 NullPointerException dans Collectors.toMap

Java 8 Collectors.toMap lève une NullPointerException si l’une des valeurs est “null”. Je ne comprends pas ce comportement, les cartes peuvent contenir des pointeurs nuls comme valeur sans aucun problème. Y a-t-il une bonne raison pour laquelle les valeurs ne peuvent pas être nulles pour Collectors.toMap ?

En outre, existe-t-il un bon moyen de résoudre ce problème avec Java 8 ou devrais-je revenir à la version ancienne pour loop?

Un exemple de mon problème:

 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(Ssortingng[] args) { List answerList = new ArrayList(); answerList.add(new Answer(1, true)); answerList.add(new Answer(2, true)); answerList.add(new Answer(3, null)); Map answerMap = answerList .stream() .collect(Collectors.toMap(Answer::getId, Answer::getAnswer)); } } 

Trace de la stack:

 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) 

Ce n’est pas possible avec les méthodes statiques de Collectors . Le javadoc de toMap explique que toMap est basé sur Map.merge :

@param mergeFunction une fonction de fusion, utilisée pour résoudre les collisions entre les valeurs associées à la même clé, fournies à la Map#merge(Object, Object, BiFunction)}

et le javadoc de Map.merge dit:

@throws NullPointerException si la clé spécifiée est null et que cette carte ne prend pas en charge les clés NULL ou la valeur ou remappingFunction est nulle

Vous pouvez éviter la boucle for en utilisant la méthode forEach de votre liste.

 Map answerMap = new HashMap<>(); answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer())); 

mais ce n’est pas vraiment simple que l’ancien:

 Map answerMap = new HashMap<>(); for (Answer answer : answerList) { answerMap.put(answer.getId(), answer.getAnswer()); } 

Tu peux le faire:

 Map collect = list.stream() .collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll); 

Ce n’est pas tellement joli, mais ça marche. Résultat:

 1: true 2: true 3: null 

( ce tutoriel m’a le plus aidé)

J’ai écrit un Collector qui, contrairement à celui par défaut de java, ne plante pas lorsque vous avez null valeurs null :

 public static  Collector> toMap(Function keyMapper, Function valueMapper) { return Collectors.collectingAndThen( Collectors.toList(), list -> { Map 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; }); } 

Il suffit de remplacer votre appel Collectors.toMap() à un appel à cette fonction et cela résoudra le problème.

Voici un collecteur un peu plus simple que celui proposé par @EmmanuelTouzery. Utilisez-le si vous aimez:

 public static  Collector> toMapNullFriendly( Function keyMapper, Function valueMapper) { @SuppressWarnings("unchecked") U none = (U) new Object(); return Collectors.collectingAndThen( Collectors. toMap(keyMapper, valueMapper.andThen(v -> v == null ? none : v)), map -> { map.replaceAll((k, v) -> v == none ? null : v); return map; }); } 

Nous remplaçons simplement null par un object personnalisé none et effectuons l’opération inverse dans le finisseur.

Oui, une réponse tardive de ma part, mais je pense que cela pourrait aider à comprendre ce qui se passe sous le capot au cas où quelqu’un voudrait coder un autre Collector Logic.

J’ai essayé de résoudre le problème en codant une approche plus native et plus simple. Je pense que c’est aussi direct que possible:

 public class LambdaUtilities { /** * In contrast to {@link Collectors#toMap(Function, Function)} the result map * may have null values. */ public static > Collector toMapWithNullValues(Function keyMapper, Function 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 > Collector toMapWithNullValues(Function keyMapper, Function valueMapper, Supplier> supplier) { return new Collector() { @Override public Supplier supplier() { return () -> { @SuppressWarnings("unchecked") M map = (M) supplier.get(); return map; }; } @Override public BiConsumer 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 combiner() { return (map1, map2) -> { map1.putAll(map2); return map1; }; } @Override public Function finisher() { return Function.identity(); } @Override public Set characteristics() { return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); } }; } } 

Et les tests utilisant JUnit et assertj:

  @Test public void testToMapWithNullValues() throws Exception { Map 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 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 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); } 

Et comment l’utilisez vous? Eh bien, utilisez-le plutôt que toMap() comme le montrent les tests. Cela rend le code d’appel aussi propre que possible.

Selon le 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) 

Quand s’appelle la map.merge

  BiConsumer accumulator = (map, element) -> map.merge(keyMapper.apply(element), valueMapper.apply(element), mergeFunction); 

Il fera une vérification null comme première chose

 if (value == null) throw new NullPointerException(); 

Je n’utilise pas Java 8 si souvent alors je ne sais pas s’il existe un meilleur moyen de le réparer, mais le réparer est un peu difficile.

Vous pourriez faire:

Utilisez filter pour filtrer toutes les valeurs NULL et, dans le code Javascript, vérifiez si le serveur n’a pas envoyé de réponse pour cet identifiant, cela signifie qu’il n’a pas répondu.

Quelque chose comme ça:

 Map answerMap = answerList .stream() .filter((a) -> a.getAnswer() != null) .collect(Collectors.toMap(Answer::getId, Answer::getAnswer)); 

Ou utilisez peek, qui est utilisé pour modifier l’élément de stream pour l’élément. En utilisant le peek, vous pouvez changer la réponse en quelque chose de plus acceptable pour la carte, mais cela signifie que vous devez modifier un peu votre logique.

On dirait que si vous souhaitez conserver le design actuel, vous devriez éviter les Collectors.toMap

Si la valeur est une chaîne, cela peut fonctionner: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

Conserver tous les identifiants de questions avec un petit tweak

 Map answerMap = answerList.stream().collect(Collectors.toMap(Answer::getId, a -> Boolean.TRUE.equals(a.getAnswer()))); 

NullPointerException est de loin l’exception la plus fréquemment rencontrée (du moins dans mon cas). Pour éviter cela, je vais sur la défensive et ajoute des tas de contrôles nuls et je finis par avoir du code grossier et laid. Java 8 introduit En option pour gérer les références NULL, vous pouvez donc définir des valeurs nullables et non nullables.

Cela dit, j’emballerais toutes les références nullables dans le conteneur facultatif. Nous devrions également ne pas briser la compatibilité en amont. Voici le code

 class Answer { private int id; private Optional 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 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(Ssortingng[] args) { List 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 (ie with null) Map> answerMapWithOptionals = answerList.stream() .collect(Collectors.toMap(Answer::getId, Answer::getAnswerAsOptional)); // map in which null values are removed Map 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 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); } }