Java 8 NullPointerException in Collectors.toMap


330

Java 8 Collectors.toMaplöst ein NullPointerExceptionif aus, wenn einer der Werte 'null' ist. Ich verstehe dieses Verhalten nicht, Maps können problemlos Nullzeiger als Wert enthalten. Gibt es einen guten Grund, warum Werte nicht null sein können Collectors.toMap?

Gibt es auch eine gute Java 8-Methode, um dies zu beheben, oder sollte ich auf die alte for-Schleife zurückgreifen?

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.


5
nullwar immer ein bisschen problematisch, wie in TreeMap. Vielleicht ein schöner Moment zum Ausprobieren Optional<Boolean>? Andernfalls teilen und Filter verwenden.
Joop Eggen

5
@JoopEggen nullkönnte ein Problem für einen Schlüssel sein, aber in diesem Fall ist es der Wert.
Pontard

Nicht alle Karten haben Problem mit null, HashMapzum Beispiel die man haben kann nullSchlüssel und eine beliebige Anzahl von nullWerten, können Sie versuchen , ein benutzerdefinierte Erstellung Collectormit ein , HashMapanstatt dem Standard eines verwenden.
Kajacx

2
@kajacx Aber die Standardimplementierung ist HashMap- wie in der ersten Zeile von Stacktrace gezeigt. Das Problem ist nicht, dass a Mapkeinen nullWert halten kann, sondern dass das zweite Argument der Map#mergeFunktion nicht null sein kann.
Czerny

Persönlich würde ich unter den gegebenen Umständen eine Nicht-Stream-Lösung oder forEach () wählen, wenn die Eingabe parallel ist. Die unten aufgeführten netten, auf kurzen Streams basierenden Lösungen könnten eine schreckliche Leistung bringen.
Ondra Žižka

Antworten:


301

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 hübsch, aber es funktioniert. Ergebnis:

1: true
2: true
3: null

( Dieses Tutorial hat mir am meisten geholfen.)


3
@Jagger Ja, eine Definition eines Lieferanten (das erste Argument) ist eine Funktion, die keine Parameter übergibt und ein Ergebnis zurückgibt. Daher besteht das Lambda für Ihren Fall () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)darin, einen StringSchlüssel ohne Berücksichtigung der Groß- und Kleinschreibung zu erstellen TreeMap.
Brett Ryan

2
Dies ist die richtige Antwort und meiner Meinung nach, was das JDK stattdessen für seine nicht überladene Standardversion tun sollte. Vielleicht ist das Zusammenführen schneller, aber ich habe es nicht getestet.
Brett Ryan

1
Ich musste die Typparameter angeben, um auf diese Weise zu kompilieren : Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);. Ich hatte:incompatible types: cannot infer type-variable(s) R (argument mismatch; invalid method reference no suitable method found for putAll(java.util.Map<java.lang.Integer,java.lang.Boolean>,java.util.Map<java.lang.Integer,java.lang.Boolean>) method java.util.Map.putAll(java.util.Map) is not applicable (actual and formal argument lists differ in length)
Anthony O.

2
Dies kann bei großen Eingaben recht langsam sein. Sie erstellen ein HashMapund rufen dann putAll()für jeden einzelnen Eintrag auf. Persönlich würde ich unter bestimmten Umständen eine Nicht-Stream-Lösung wählen oder forEach()wenn die Eingabe parallel ist.
Ondra Žižka

3
Beachten Sie, dass sich diese Lösung anders verhält als die ursprüngliche toMap-Implementierung. Die ursprüngliche Implementierung erkennt doppelte Schlüssel und löst eine IllegalStatException aus. Diese Lösung akzeptiert jedoch stillschweigend den neuesten Schlüssel. Die Lösung von Emmanuel Touzery ( stackoverflow.com/a/32648397/471214 ) kommt dem ursprünglichen Verhalten näher.
mmdemirbas

173

Mit den statischen Methoden von ist dies nicht möglich Collectors. Das Javadoc von toMaperklärt, toMapdas basiert auf Map.merge:

@param mergeFunction Eine Zusammenführungsfunktion, mit der Kollisionen zwischen Werten aufgelöst werden, die demselben Schlüssel zugeordnet sind, wie an Map#merge(Object, Object, BiFunction)}

und der Javadoc von Map.mergesagt:

@throws Nullpointer wenn der angegebene Schlüssel ist null und diese Karte null Schlüssel oder nicht unterstützt wird der Wert oder remappingFunction ist null

Sie können die for-Schleife mithilfe der forEachMethode Ihrer Liste vermeiden .

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());
}

3
In diesem Fall würde ich lieber das altmodische für jeden verwenden. Sollte ich dies als Fehler in toMerge betrachten? Da die Verwendung dieser Zusammenführungsfunktion wirklich ein Implementierungsdetail ist, oder ist dies eine gute Begründung dafür, dass toMap keine Nullwerte verarbeiten darf?
Jasper

6
Es ist im Javadoc der Zusammenführung angegeben, aber es ist nicht im Dokument von toMap
Jasper

119
Ich hätte nie gedacht, dass Nullwerte in der Karte einen solchen Einfluss auf die Standard-API haben würden, ich würde es eher als Fehler betrachten.
Askar Kalykov

16
Tatsächlich geben die API-Dokumente nichts über die Verwendung von an Map.merge. Dieses IMHO ist ein Fehler in der Implementierung, der einen völlig akzeptablen Anwendungsfall einschränkt, der übersehen wurde. Die überladenen Methoden von toMapdo geben die Verwendung von an, Map.mergeaber nicht die, die das OP verwendet.
Brett Ryan

11
@ Jasper gibt es sogar Fehlerbericht bugs.openjdk.java.net/browse/JDK-8148463
Pixel

23

Ich habe eine geschrieben, Collectordie im Gegensatz zur Standard-Java nicht abstürzt, wenn Sie nullWerte 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()Anruf durch einen Aufruf dieser Funktion, und das Problem wird behoben.


1
Aber das Zulassen von nullWerten und das Verwenden putIfAbsentspielen nicht gut zusammen. Es erkennt keine doppelten Schlüssel, wenn sie null
Holger

10

Ja, eine späte Antwort von mir, aber ich denke, es kann hilfreich sein zu verstehen, was unter der Haube passiert, falls jemand eine andere CollectorLogik codieren möchte.

Ich habe versucht, das Problem zu lösen, indem ich einen nativeren und direkteren Ansatz codiert habe. 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 (left, right) -> {
          int total = left.size() + right.size();
          left.putAll(right);
          if (left.size() < total) {
            throw new IllegalStateException("Duplicate key(s)");
          }
          return left;
        };
      }

      @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);
  }

  @Test
  public void testToMapWithNullValuesParallelWithDuplicates() throws Exception {
    assertThatThrownBy(() -> Stream.of(1, 2, 3, 1, 2, 3)
        .parallel() // this causes .combiner() to be called
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
            .isExactlyInstanceOf(IllegalStateException.class)
            .hasCauseExactlyInstanceOf(IllegalStateException.class)
            .hasStackTraceContaining("Duplicate key");
  }

Und wie benutzt du es? Verwenden Sie es einfach, anstatt toMap()wie in den Tests gezeigt. Dadurch sieht der aufrufende Code so sauber wie möglich aus.

BEARBEITEN:
Holgers Idee unten implementiert, eine Testmethode hinzugefügt


1
Der Kombinierer sucht nicht nach doppelten Schlüsseln. Wenn Sie vermeiden möchten, nach jedem Schlüssel zu suchen, können Sie etwas wie(map1, map2) -> { int total = map1.size() + map2.size(); map1.putAll(map2); if(map1.size() < total.size()) throw new IllegalStateException("Duplicate key(s)"); return map1; }
Holger

@ Holger Ja, das stimmt. Zumal das accumulator()eigentlich prüft. Vielleicht sollte ich einmal einige parallele Streams machen :)
sjngm

7

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 nur durch nullein benutzerdefiniertes Objekt noneund führen den umgekehrten Vorgang im Finisher durch.


5

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


4
Das funktioniert nur, wenn Sie mit dem Ändern der Daten einverstanden sind. Nachgeschaltete Methoden erwarten möglicherweise eher Nullwerte als leere Zeichenfolgen.
Sam Buchmiller

3

Laut der 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 das map.merge

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

Es wird nullals erstes eine Überprüfung durchführen

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

Ich benutze Java 8 nicht so oft, daher weiß ich nicht, ob es einen besseren Weg gibt, es zu reparieren, aber es ist ein bisschen schwierig, es zu reparieren.

Du könntest es tun:

Verwenden Sie den Filter, um alle NULL-Werte zu filtern. Wenn Sie im Javascript-Code prüfen, ob der Server keine Antwort auf diese ID gesendet hat, bedeutet dies, dass er nicht darauf geantwortet 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, mit dem das Stream-Element für Element geändert wird. Mit peek können Sie die Antwort in etwas ändern, das für die Karte akzeptabler ist, aber es bedeutet, dass Sie Ihre Logik ein wenig bearbeiten.

Klingt so, als ob Sie das aktuelle Design beibehalten möchten, das Sie vermeiden sollten Collectors.toMap


3

Ich habe die Implementierung von Emmanuel Touzery leicht modifiziert .

Diese Version;

  • Ermöglicht Nullschlüssel
  • Ermöglicht Nullwerte
  • Erkennt doppelte Schlüssel (auch wenn sie null sind) und löst IllegalStateException wie in der ursprünglichen JDK-Implementierung aus.
  • Erkennt doppelte Schlüssel auch dann, wenn der Schlüssel bereits dem Nullwert zugeordnet ist. Mit anderen Worten, trennt eine Zuordnung mit Nullwert von der Nichtzuordnung.
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))
    );
}

1

Es tut mir leid, eine alte Frage erneut zu öffnen, aber da sie kürzlich bearbeitet wurde und besagt, dass das "Problem" immer noch in Java 11 besteht, wollte ich darauf hinweisen:

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

gibt Ihnen die Nullzeigerausnahme, da die Zuordnung keinen Nullwert zulässt. Dies ist sinnvoll k, da der zurückgegebene Wert bereits vorhanden ist, wenn Sie in einer Karte nach dem Schlüssel suchen und dieser nicht vorhanden ist null(siehe javadoc). Wenn Sie also kden Wert nulleingeben könnten, würde die Karte so aussehen, als würde sie sich merkwürdig 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 nullWerte in die Karte eingefügt, und STILL erhalten Sie nullals "Wert", wenn Sie nach einer ID suchen, die keine Antwort in der Karte hat.

Ich hoffe das macht für alle Sinn.


1
Es wäre sinnvoll, wenn eine Karte keine Nullwerte zulässt, aber dies ist der Fall. Sie können answerMap.put(4, null);ohne Probleme auskommen. Sie haben Recht, dass Sie mit Ihrer vorgeschlagenen Lösung das gleiche Ergebnis für anserMap.get () erhalten, wenn es nicht vorhanden ist, als würde der Wert als null eingefügt. Wenn Sie jedoch alle Einträge der Karte durchlaufen, gibt es offensichtlich einen Unterschied.
Jasper

Es ist die einfachste Lösung für meinen Fall, danke
ahmkara

1
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());
}

1
Upvoting, weil dies kompiliert. Akzeptierte Antwort wird nicht kompiliert, da Map :: putAll keinen Rückgabewert hat.
Taugenichts

0

Beibehaltung aller Fragen-IDs mit kleinen Änderungen

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

Ich denke, dies ist die beste Antwort - es ist die präziseste Antwort und behebt das NPE-Problem.
LConrad

-3

NullPointerException ist bei weitem die am häufigsten auftretende Ausnahme (zumindest in meinem Fall). Um dies zu vermeiden, gehe ich in die Defensive und füge eine Reihe von Nullprüfungen hinzu. Am Ende habe ich aufgeblähten und hässlichen Code. Java 8 führt Optional ein, um Nullreferenzen zu verarbeiten, damit Sie nullbare und nicht nullbare Werte definieren können.

Das heißt, ich würde alle nullbaren Referenzen in den optionalen Container einschließen. Wir sollten auch nicht die Abwärtskompatibilität brechen. Hier ist der Code.

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);
    }
}

1
nutzlose Antwort, warum sollten Sie null loswerden, um dies zu beheben? Dies ist ein Problem von Collectors.toMap()nicht null Werten
Enerccio

@Enerccio beruhige Kumpel !! Es ist keine gute Praxis, sich auf Nullwerte zu verlassen. Wenn Sie Optional verwendet hätten, wären Sie NPE überhaupt nicht begegnet. Informieren Sie sich über optionale Verwendungen.
TriCore

1
und warum ist das? Der Nullwert ist in Ordnung, es ist die undokumentierte Bibliothek, die das Problem darstellt. Optional ist schön aber nicht überall.
Enerccio
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.