Java 8 Collectors.toMap
renvoie 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 null pour Collectors.toMap
?
De plus, existe-t-il une solution de Nice Java 8 permettant de résoudre ce problème ou devrais-je revenir à plain old?
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(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));
}
}
Trace de la pile:
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 problème existe toujours dans Java 11.
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é, telle que fournie à
Map#merge(Object, Object, BiFunction)}
et le javadoc de Map.merge
dit:
@throws NullPointerException si la clé spécifiée est null et cette carte ne prend pas en charge les clés nulles ni la valeur ou remappingFunction estnul
Vous pouvez éviter la boucle for en utilisant la méthode forEach
de votre liste.
Map<Integer, Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));
mais ce n'est pas vraiment simple que l'ancienne manière:
Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
answerMap.put(answer.getId(), answer.getAnswer());
}
J'ai écrit une Collector
qui, contrairement à celle par défaut de Java, ne plante pas lorsque vous avez des valeurs 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;
});
}
Il suffit de remplacer votre appel Collectors.toMap()
à un appel de cette fonction et le problème sera résolu.
Oui, une réponse tardive de ma part, mais je pense qu'il peut être utile de comprendre ce qui se passe sous le capot au cas où quelqu'un voudrait coder une autre logique Collector
-.
J'ai essayé de résoudre le problème en codant une approche plus native et directe. 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 <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));
}
};
}
}
Et les tests utilisant JUnit et 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);
}
Et comment l'utilisez-vous? Eh bien, utilisez-le au lieu de toMap()
, comme le montrent les tests. Cela rend le code d'appel aussi propre que possible.
Voici un collectionneur un peu plus simple que celui proposé par @EmmanuelTouzery. Utilisez-le si vous aimez:
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;
});
}
Nous remplaçons simplement null
par un objet personnalisé none
et effectuons l'opération inverse dans le module de finition.
Si la valeur est une chaîne, cela pourrait alors fonctionner:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
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 le map.merge
BiConsumer<M, T> 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 souvent Java 8, donc 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. Dans le code Javascript, vérifiez si le serveur n'a pas envoyé de réponse car cet identifiant signifie qu'il n'a pas répondu.
Quelque chose comme ça:
Map<Integer, Boolean> 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 flux pour l'élément. En utilisant Peek, vous pouvez changer la réponse en une chose plus acceptable pour la carte, mais cela signifie modifier un peu votre logique.
On dirait que si vous voulez conserver la conception actuelle, vous devriez éviter Collectors.toMap
J'ai légèrement modifié implémentation d'Emmanuel Touzery .
Cette version;
public static <T, K, U> Collector<T, ?, Map<K, U>> toMapOfNullables(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) {
return Collectors.collectingAndThen(
Collectors.toList(),
list -> {
Map<K, U> map = new LinkedHashMap<>();
list.forEach(item -> {
K key = keyMapper.apply(item);
if (map.containsKey(key)) {
throw new IllegalStateException(String.format("Duplicate key %s", key));
}
map.put(key, valueMapper.apply(item));
});
return map;
}
);
}
Tests unitaires:
@Test
public void toMapOfNullables_WhenHasNullKey() {
assertEquals(singletonMap(null, "value"),
Stream.of("ignored").collect(Utils.toMapOfNullables(i -> null, i -> "value"))
);
}
@Test
public void toMapOfNullables_WhenHasNullValue() {
assertEquals(singletonMap("key", null),
Stream.of("ignored").collect(Utils.toMapOfNullables(i -> "key", i -> null))
);
}
@Test
public void toMapOfNullables_WhenHasDuplicateNullKeys() {
assertThrows(new IllegalStateException("Duplicate key null"),
() -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> null, i -> i))
);
}
@Test
public void toMapOfNullables_WhenHasDuplicateKeys_NoneHasNullValue() {
assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
() -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
);
}
@Test
public void toMapOfNullables_WhenHasDuplicateKeys_OneHasNullValue() {
assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
() -> Stream.of(1, null, 3).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
);
}
@Test
public void toMapOfNullables_WhenHasDuplicateKeys_AllHasNullValue() {
assertThrows(new IllegalStateException("Duplicate key duplicated-key"),
() -> Stream.of(null, null, null).collect(Utils.toMapOfNullables(i -> "duplicated-key", i -> i))
);
}
public static <T, K, V> Collector<T, HashMap<K, V>, HashMap<K, V>> toHashMap(
Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends V> valueMapper
)
{
return Collector.of(
HashMap::new,
(map, t) -> map.put(keyMapper.apply(t), valueMapper.apply(t)),
(map1, map2) -> {
map1.putAll(map2);
return map1;
}
);
}
public static <T, K> Collector<T, HashMap<K, T>, HashMap<K, T>> toHashMap(
Function<? super T, ? extends K> keyMapper
)
{
return toHashMap(keyMapper, Function.identity());
}
Désolé de rouvrir une vieille question, mais comme elle a été modifiée récemment en disant que le "problème" est toujours présent dans Java 11, je me suis dit que je voulais signaler ceci:
answerList
.stream()
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
vous donne l'exception du pointeur null car la carte n'autorise pas null en tant que valeur . Cela est logique car si vous recherchez dans la carte la clé k
et qu'elle n'est pas présente, la valeur renvoyée est déjà null
(voir javadoc ). Donc, si vous pouviez mettre dans k
la valeur null
, la carte aurait l'air de se comporter étrangement.
Comme quelqu'un l'a dit dans les commentaires, il est assez facile de résoudre ce problème en utilisant le filtrage:
answerList
.stream()
.filter(a -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
de cette manière, aucune valeur null
ne sera insérée dans la carte, et ENCORE, vous obtiendrez null
comme "valeur" lorsque vous rechercherez un identifiant qui n'a pas de réponse dans la carte.
J'espère que cela a du sens pour tout le monde.
Conserver toutes les identifiants de questions avec un petit Tweak
Map<Integer, Boolean> answerMap =
answerList.stream()
.collect(Collectors.toMap(Answer::getId, a ->
Boolean.TRUE.equals(a.getAnswer())));