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.

541voto

kajacx Punkte 11261

Du kannst diesen bekannten Fehler in OpenJDK umgehen mit folgendem:

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

EDIT:

Im Gegensatz zu Collectors.toMap werden Werte hier stillschweigend ersetzt, wenn du den gleichen Schlüssel mehrmals hast, wie @mmdemirbas in den Kommentaren angemerkt hat. Wenn du das nicht möchtest, schau dir den Link im Kommentar an.

212voto

gontard Punkte 27500

Es ist mit den statischen Methoden von Collectors nicht möglich. Der Javadoc von toMap erklärt, dass toMap auf Map.merge basiert:

@param mergeFunction eine Merge-Funktion, die zur Auflösung von Kollisionen zwischen Werten verwendet wird, die mit demselben Schlüssel verbunden sind, wie sie an Map#merge(Object, Object, BiFunction) übergeben werden

und der Javadoc von Map.merge besagt:

@throws NullPointerException, wenn der angegebene Schlüssel null ist und diese Map null-Schlüssel nicht unterstützt oder der Wert oder remappingFunction null ist

Sie können die For-Schleife vermeiden, indem Sie die forEach-Methode Ihrer Liste verwenden.

Map answerMap = new HashMap<>();
answerList.forEach((answer) -> answerMap.put(answer.getId(), answer.getAnswer()));

aber es ist nicht wirklich einfacher als auf die altmodische Weise:

Map answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}

34voto

Emmanuel Touzery Punkte 8370

Ich habe einen Collector geschrieben, der im Gegensatz zum standardmäßigen Java-Collector nicht abstürzt, wenn Sie null-Werte haben:

public static 
        Collector> toMap(Function keyMapper,
                Function valueMapper) {
    return Collectors.collectingAndThen(
            Collectors.toList(),
            list -> {
                Map 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("Doppelter Schlüssel %s", key));
                    }
                }
                return result;
            });
}

Ersetzen Sie einfach Ihren Collectors.toMap()-Aufruf durch einen Aufruf dieser Funktion, und das Problem wird behoben.

13voto

Tagir Valeev Punkte 91975

Hier ist ein etwas einfacherer Sammler als der von @EmmanuelTouzery vorgeschlagene. Verwenden Sie ihn, wenn Sie möchten:

public static  Collector> toMapNullFriendly(
        Function keyMapper,
        Function valueMapper) {
    @SuppressWarnings("unchecked")
    U none = (U) new Object();
    return Collectors.collectingAndThen(
            Collectors. 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 die umgekehrte Operation im Finisher aus.

13voto

sjngm Punkte 11845

Ja, eine späte Antwort von mir, aber ich denke, es kann helfen zu verstehen, was hinter den Kulissen passiert, falls jemand eine andere Collector-Logik codieren möchte.

Ich habe versucht, das Problem zu lösen, indem ich einen nativeren und geradlinigeren Ansatz programmiert habe. Ich denke, es ist so direkt wie möglich:

public class LambdaUtilities {

  /**
   * Im Gegensatz zu {@link Collectors#toMap(Function, Function)} kann die resultierende Map
   * null-Werte enthalten.
   */
  public static > Collector toMapWithNullValues(Function keyMapper, Function valueMapper) {
    return toMapWithNullValues(keyMapper, valueMapper, HashMap::new);
  }

  /**
   * Im Gegensatz zu {@link Collectors#toMap(Function, Function, BinaryOperator, Supplier)}
   * kann die resultierende Map null-Werte enthalten.
   */
  public static > Collector toMapWithNullValues(Function keyMapper, Function valueMapper, Supplier> supplier) {
    return new Collector() {

      @Override
      public Supplier supplier() {
        return () -> {
          @SuppressWarnings("unchecked")
          M map = (M) supplier.get();
          return map;
        };
      }

      @Override
      public BiConsumer accumulator() {
        return (map, element) -> {
          K key = keyMapper.apply(element);
          if (map.containsKey(key)) {
            throw new IllegalStateException("Doppelter Schlüssel " + key);
          }
          map.put(key, valueMapper.apply(element));
        };
      }

      @Override
      public BinaryOperator combiner() {
        return (left, right) -> {
          int total = left.size() + right.size();
          left.putAll(right);
          if (left.size() < total) {
            throw new IllegalStateException("Doppelter(s) Schlüssel");
          }
          return left;
        };
      }

      @Override
      public Function finisher() {
        return Function.identity();
      }

      @Override
      public Set characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
      }

    };
  }

}

Und die Tests mit JUnit und assertj:

  @Test
  public void testToMapWithNullValues() throws Exception {
    Map 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 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("Doppelter Schlüssel 1");
  }

  @Test
  public void testToMapWithNullValuesParallel() throws Exception {
    Map result = Stream.of(1, 2, 3)
        .parallel() // dadurch wird .combiner() aufgerufen
        .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() // dadurch wird .combiner() aufgerufen
        .collect(LambdaUtilities.toMapWithNullValues(Function.identity(), x -> x % 2 == 1 ? x : null)))
            .isExactlyInstanceOf(IllegalStateException.class)
            .hasCauseExactlyInstanceOf(IllegalStateException.class)
            .hasStackTraceContaining("Doppelter Schlüssel");
  }

Und wie benutzt man es? Nun, einfach anstelle von toMap() verwenden, wie es die Tests zeigen. So sieht der Aufrufcode so sauber wie möglich aus.

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

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