web-dev-qa-db-de.com

Java 8 NullPointerException in Collectors.toMap

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.

225
Jasper

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

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.)

177
kajacx

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.

16

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.

7
sjngm

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.

5
Tagir Valeev

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

4
Gnana

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.

3
Marco Acierno

Ich habe die Implementierung von Emmanuel Touzery leicht modifiziert .

Diese Version;

  • Ermöglicht Nullschlüssel
  • Ermöglicht Nullwerte
  • Erkennt doppelte Schlüssel (auch wenn diese null sind) und löst wie in der ursprünglichen JDK-Implementierung eine IllegalStateException 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 keiner Zuordnung.
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))
    );
}
0
mmdemirbas
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());
}
0
Igor Zubchenok

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.

0
Luca

Beibehaltung aller Fragen-IDs mit kleinem Tweak

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