566 Stimmen

NullPointerException in Collectors.toMap with null entry values NullPointerException in Collectors.toMap mit null Eintragswerten

Collectors.toMap wirft eine NullPointerException, wenn einer der Werte null ist. Ich verstehe dieses Verhalten nicht, Maps können problemlos Null-Pointer als Wert enthalten. Gibt es einen guten Grund, warum Werte für Collectors.toMap nicht null sein können?

Gibt es auch einen schönen Java 8 Weg, um dieses Problem zu beheben, oder sollte ich wieder zur einfachen 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 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));
    }
}

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 auch in Java 11 weiter.

10voto

mmdemirbas Punkte 8840

Ich habe Emmanuel Touzerys null-sichere Collector-Implementierung leicht modifiziert.

Diese Version:

  • Erlaubt Nullschlüssel
  • Erlaubt Nullwerte
  • Erkennt doppelte Schlüssel (auch wenn sie null sind) und wirft IllegalStateException, wie in der originalen JDK-Implementierung
  • Erkennt auch doppelte Schlüssel, wenn der Schlüssel bereits auf den Nullwert abgebildet ist. Mit anderen Worten, trennt ein Mapping mit Nullwert von keinem Mapping

    public static Collector> toMapOfNullables(Function keyMapper, Function valueMapper) { return Collectors.collectingAndThen( Collectors.toList(), list -> { Map map = new LinkedHashMap<>(); list.forEach(item -> { K key = keyMapper.apply(item); U value = valueMapper.apply(item); if (map.containsKey(key)) { throw new IllegalStateException(String.format( "Doppelter Schlüssel %s (Versuch, Werte %s und %s zu fusionieren)", key, map.get(key), value)); } map.put(key, value); }); return map; } ); }

Unit-Tests:

@Test
public void toMapOfNullables_WhenHasNullKey() {
    assertEquals(singletonMap(null, "Wert"),
        Stream.of("ignoriert").collect(Utils.toMapOfNullables(i -> null, i -> "Wert"))
    );
}

@Test
public void toMapOfNullables_WhenHasNullValue() {
    assertEquals(singletonMap("Schlüssel", null),
        Stream.of("ignoriert").collect(Utils.toMapOfNullables(i -> "Schlüssel", i -> null))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateNullKeys() {
    assertThrows(new IllegalStateException("Doppelter Schlüssel null"),
        () -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> null, i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_NoneHasNullValue() {
    assertThrows(new IllegalStateException("Doppelter Schlüssel duplizierter-Schlüssel"),
        () -> Stream.of(1, 2, 3).collect(Utils.toMapOfNullables(i -> "duplizierter-Schlüssel", i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_OneHasNullValue() {
    assertThrows(new IllegalStateException("Doppelter Schlüssel duplizierter-Schlüssel"),
        () -> Stream.of(1, null, 3).collect(Utils.toMapOfNullables(i -> "duplizierter-Schlüssel", i -> i))
    );
}

@Test
public void toMapOfNullables_WhenHasDuplicateKeys_AllHasNullValue() {
    assertThrows(new IllegalStateException("Doppelter Schlüssel duplizierter-Schlüssel"),
        () -> Stream.of(null, null, null).collect(Utils.toMapOfNullables(i -> "duplizierter-Schlüssel", i -> i))
    );
}

5voto

Luca Punkte 1136

Entschuldigung, dass ich eine alte Frage wieder öffne, aber da sie kürzlich bearbeitet wurde und gesagt wurde, dass das "Problem" in Java 11 immer noch besteht, wollte ich darauf hinweisen:

answerList
        .stream()
        .collect(Collectors.toMap(Answer::getId, Answer::getAnswer));

gibt Ihnen die Null-Zeiger-Ausnahme, weil die Map keinen Nullwert zulässt. Das macht Sinn, denn wenn Sie in einer Map nach dem Schlüssel k suchen und er nicht vorhanden ist, ist der zurückgegebene Wert bereits null (siehe javadoc). Wenn Sie also den Wert null für k einfügen könnten, würde die Map 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 Map eingefügt, und trotzdem erhalten Sie null als "Wert", wenn Sie nach einer ID suchen, die keine Antwort in der Map hat.

Ich hoffe, das ergibt für alle Sinn.

4voto

Gnana Punkte 606

Wenn der Wert ein String ist, könnte dies funktionieren: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

4voto

Igor Zubchenok Punkte 675
public static  Collector, HashMap> toHashMap(
    Function keyMapper,
    Function 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  Collector, HashMap> toHashMap(
    Function keyMapper
)
{
    return toHashMap(keyMapper, Function.identity());
}

3voto

Marco Acierno Punkte 14672

Gemäß dem Stacktrace

Ausnahme im Thread "main" java.lang.NullPointerException
bei java.util.HashMap.merge(HashMap.java:1216)
bei java.util.stream.Collectors.lambda$toMap$148(Collectors.java:1320)
bei java.util.stream.Collectors$$Lambda$5/391359742.accept(Unknown Source)
bei java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.java:169)
bei java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1359)
bei java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
bei java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
bei java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:708)
bei java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
bei java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
bei com.guice.Main.main(Main.java:28)
bei sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
bei sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
bei sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
bei java.lang.reflect.Method.invoke(Method.java:483)
bei com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

Wenn map.merge aufgerufen wird

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

Es wird zuerst eine null Überprüfung durchgeführt

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

Ich benutze Java 8 nicht so oft, also weiß ich nicht, ob es einen besseren Weg gibt, es zu beheben, aber die Behebung ist ein wenig schwierig.

Sie könnten Folgendes tun:

Verwenden Sie Filter, um alle NULL-Werte zu filtern, und im JavaScript-Code überprüfen, ob der Server für diese ID keine Antwort gesendet hat, bedeutet, dass er nicht darauf geantwortet hat.

So etwas wie dies:

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

Oder verwenden Sie peek, das verwendet wird, um das Stream-Element für Element zu ändern. Mit peek könnten Sie die Antwort in etwas Akzeptableres für die Karte ändern, aber das bedeutet, dass Sie Ihre Logik etwas bearbeiten müssen.

Es scheint, als ob Sie bei Beibehaltung des aktuellen Designs Collectors.toMap vermeiden sollten

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X