Java 8 Collectors.toMap
löst eine NullPointerException
aus, wenn einer der Werte 'null' ist. Ich verstehe dieses Verhalten nicht, Karten können ohne Probleme Nullzeiger als Wert enthalten. Gibt es einen guten Grund, warum Werte für Collectors.toMap
nicht null sein können?
Gibt es eine Nice-Java-8-Methode, um das Problem zu beheben, oder sollte ich zur normalen alten for-Schleife zurückkehren?
Ein Beispiel für mein Problem:
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)
Dieses Problem besteht weiterhin in Java 11.
Mit den statischen Methoden von Collectors
ist dies nicht möglich. Der Javadoc von toMap
erklärt, dass toMap
auf Map.merge
basiert:
@param mergeFunction Eine Zusammenführungsfunktion, die zum Auflösen von Kollisionen zwischen Werten verwendet wird, die demselben Schlüssel zugeordnet sind, wie in
Map#merge(Object, Object, BiFunction)}
angegeben.
und der Javadoc von Map.merge
sagt:
@throws NullPointerException, wenn der angegebene Schlüssel null ist und diese Zuordnung lautet unterstützt keine Nullschlüssel oder der Wert oder die RemappingFunction istNull
Sie können die for-Schleife vermeiden, indem Sie die forEach
-Methode Ihrer Liste verwenden.
Map<Integer, Boolean> answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));
aber es ist nicht wirklich einfach als der alte Weg:
Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
answerMap.put(answer.getId(), answer.getAnswer());
}
Sie können diesen bekannten Fehler in OpenJDK folgendermaßen umgehen:
Map<Integer, Boolean> collect = list.stream()
.collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);
Es ist nicht so schön, aber es funktioniert. Ergebnis:
1: true
2: true
3: null
( dieses Tutorial hat mir am meisten geholfen.)
Ich habe eine Collector
geschrieben, die im Gegensatz zu der Standard-Java-Version nicht abstürzt, wenn Sie null
-Werte haben:
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;
});
}
Ersetzen Sie einfach Ihren Collectors.toMap()
-Aufruf für einen Aufruf dieser Funktion, um das Problem zu beheben.
Ja, eine späte Antwort von mir, aber ich denke, es kann helfen zu verstehen, was unter der Haube passiert, falls jemand eine andere Collector
- Logik programmieren möchte.
Ich habe versucht, das Problem zu lösen, indem ich einen nativeren und direkteren Ansatz programmiere. Ich denke es ist so direkt wie möglich:
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));
}
};
}
}
Und die Tests mit JUnit und 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);
}
Und wie benutzt du es? Nun, verwenden Sie es einfach anstelle von toMap()
, wie die Tests zeigen. Dadurch erscheint der aufrufende Code so sauber wie möglich.
Hier ist ein etwas einfacherer Sammler als von @EmmanuelTouzery vorgeschlagen. Verwenden Sie es, wenn Sie möchten:
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;
});
}
Wir ersetzen einfach null
durch ein benutzerdefiniertes Objekt none
und führen den umgekehrten Vorgang im Finisher aus.
Wenn der Wert ein String ist, kann dies funktionieren:
map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))
Laut 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)
Wann heißt der map.merge
BiConsumer<M, T> accumulator
= (map, element) -> map.merge(keyMapper.apply(element),
valueMapper.apply(element), mergeFunction);
Als erstes wird eine null
-Prüfung durchgeführt
if (value == null)
throw new NullPointerException();
Ich benutze Java 8 nicht so oft, also weiß ich nicht, ob es eine bessere Lösung gibt, aber es ist ein bisschen schwierig.
Du könntest es tun:
Verwenden Sie einen Filter, um alle NULL-Werte zu filtern. Überprüfen Sie im Javascript-Code, ob der Server für diese ID keine Antwort gesendet hat. Dies bedeutet, dass er darauf nicht reagiert hat.
Etwas wie das:
Map<Integer, Boolean> answerMap =
answerList
.stream()
.filter((a) -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
Oder verwenden Sie peek, um das Stream-Element für Element zu ändern. Mit Hilfe von Peek können Sie die Antwort in etwas ändern, das für die Map akzeptabler ist. Dies bedeutet jedoch, dass Sie Ihre Logik ein wenig bearbeiten müssen.
Hört sich an, wenn Sie das aktuelle Design beibehalten möchten, sollten Sie Collectors.toMap
vermeiden.
Ich habe die Implementierung von Emmanuel Touzery leicht modifiziert .
Diese 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;
}
);
}
Unit-Tests:
@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());
}
Es tut uns leid, eine alte Frage erneut zu öffnen, aber da sie kürzlich bearbeitet wurde und sagt, dass das "Problem" immer noch in Java 11 enthalten ist, wollte ich darauf hinweisen:
answerList
.stream()
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
gibt die Nullzeiger-Ausnahme aus, da die Map keine Null als Wert zulässt .. Dies ist sinnvoll, da der zurückgegebene Wert bereits k
ist, wenn Sie in einer Map nach dem Schlüssel null
suchen und er nicht vorhanden ist ). Wenn Sie also in k
den Wert null
eingeben könnten, würde die Karte so aussehen, als würde sie sich seltsam verhalten.
Wie jemand in den Kommentaren sagte, ist es ziemlich einfach, dies durch Filtern zu lösen:
answerList
.stream()
.filter(a -> a.getAnswer() != null)
.collect(Collectors.toMap(Answer::getId, Answer::getAnswer));
auf diese Weise werden keine null
-Werte in die Karte eingefügt. STILL erhalten Sie null
als "Wert", wenn Sie nach einer ID suchen, die keine Antwort in der Karte enthält.
Ich hoffe das macht für jeden Sinn.
Beibehaltung aller Fragen-IDs mit kleinem Tweak
Map<Integer, Boolean> answerMap =
answerList.stream()
.collect(Collectors.toMap(Answer::getId, a ->
Boolean.TRUE.equals(a.getAnswer())));