Java 8 NullPointerException w Collectors.toMap

331

Java 8 Collectors.toMapzgłasza, NullPointerExceptionjeśli jedna z wartości ma wartość „null”. Nie rozumiem tego zachowania, mapy mogą zawierać zerowe wskaźniki jako wartość bez żadnych problemów. Czy istnieje dobry powód, dla którego wartości nie mogą mieć wartości null Collectors.toMap?

Czy jest też dobry sposób na naprawienie tego w Javie 8, czy powinienem powrócić do zwykłej starej pętli for?

Przykład mojego problemu:

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

Ślad stosu:

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)

Ten problem nadal występuje w Javie 11.

Jaspis
źródło
5
nullzawsze było trochę problematyczne, jak w TreeMap. Może miły moment na wypróbowanie Optional<Boolean>? W przeciwnym razie podziel i użyj filtru.
Joop Eggen
5
@JoopEggen nullmoże stanowić problem dla klucza, ale w tym przypadku jest to wartość.
gontard
Nie wszystkie mapy mają problem null, HashMapna przykład może mieć jeden nullklucz i dowolną liczbę nullwartości, możesz spróbować utworzyć niestandardowy Collectorza pomocą HashMapzamiast zamiast domyślnego.
kajacx
2
@kajacx Ale domyślną implementacją jest HashMap- jak pokazano w pierwszym wierszu stacktrace. Problemem nie jest to, że nie Mapmożna przechowywać nullwartości, ale że drugi argument Map#mergefunkcji nie może mieć wartości NULL.
czerny
Osobiście, w danych okolicznościach, wybrałbym rozwiązanie inne niż stream lub forEach (), jeśli dane wejściowe są równoległe. Poniższe fajne rozwiązania oparte na krótkim strumieniu mogą mieć fatalną wydajność.
Ondra Žižka

Odpowiedzi:

301

Możesz obejść ten znany błąd w OpenJDK za pomocą:

Map<Integer, Boolean> collect = list.stream()
        .collect(HashMap::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap::putAll);

Nie jest aż tak ładnie, ale działa. Wynik:

1: true
2: true
3: null

( ten samouczek najbardziej mi pomógł).

kajacx
źródło
3
@Jagger tak, definicja dostawcy (pierwszy argument) jest funkcją, która nie przekazuje żadnych parametrów i zwraca wynik, więc lambda dla twojej sprawy oznaczałoby () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)utworzenie Stringklucza bez rozróżniania wielkości liter TreeMap.
Brett Ryan,
2
To jest prawidłowa odpowiedź i IMHO, co powinien zrobić JDK dla swojej domyślnej, nie przeciążonej wersji. Może scalanie jest szybsze, ale nie testowałem.
Brett Ryan
1
Musiałem określić parametry typu, w celu sporządzenia, w ten sposób: Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);. Miałem: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
Może to być dość powolne przy dużych wejściach. Tworzysz, HashMapa następnie dzwonisz putAll()do każdego wpisu. Osobiście w danych okolicznościach wybrałbym rozwiązanie inne niż strumieniowe lub forEach()jeśli dane wejściowe są równoległe.
Ondra Žižka
3
Uważaj, że to rozwiązanie zachowuje się inaczej niż oryginalna implementacja toMap. Oryginalna implementacja wykrywa duplikaty kluczy i zgłasza wyjątek IllegalStatException, ale to rozwiązanie cicho akceptuje najnowszy klucz. Rozwiązanie Emmanuela Touzery'ego ( stackoverflow.com/a/32648397/471214 ) jest bliższe pierwotnemu zachowaniu.
mmdemirbas
174

Nie jest to możliwe przy użyciu metod statycznych Collectors. Javadoc toMapwyjaśnia, że toMapopiera się na Map.merge:

@param mergeFunkcja funkcja scalania, używana do rozwiązywania kolizji między wartościami powiązanymi z tym samym kluczem, w jakim została dostarczona Map#merge(Object, Object, BiFunction)}

a javadoc Map.mergemówi:

@ rzuca NullPointerException, jeśli określony klucz ma wartość null, a mapa nie obsługuje kluczy null lub wartość lub funkcja odwzorowania ma wartość null

Możesz uniknąć pętli for, używając forEachmetody z listy.

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

ale nie jest to tak naprawdę proste jak dawniej:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}
gontard
źródło
3
W takim przypadku wolałbym użyć staromodnego dla każdego. Czy powinienem uznać to za błąd w toMerge? ponieważ użycie tej funkcji scalania jest tak naprawdę szczegółem implementacji, czy też jest dobrym powodem, dla którego nie można pozwolić, aby toMap przetwarzało wartości zerowe?
Jasper
6
Jest to określone w javadoc scalania, ale nie zostało to określone w dokumencie toMap
Jasper
119
Nigdy nie myślałem, że wartości zerowe na mapie będą miały taki wpływ na standardowy interfejs API, wolę raczej uznać to za wadę.
Askar Kalykov
16
W rzeczywistości dokumenty API nie zawierają żadnych informacji na temat używania Map.merge. Ten IMHO jest wadą w implementacji, która ogranicza całkowicie dopuszczalny przypadek użycia, który został pominięty. Przeciążone metody toMapokreślają użycie, Map.mergeale nie tego, którego używa OP.
Brett Ryan,
11
@Jasper jest nawet zgłoszenie błędu bugs.openjdk.java.net/browse/JDK-8148463
piksel
23

Napisałem taki, Collectorktóry, w przeciwieństwie do domyślnego java, nie ulega awarii, gdy masz nullwartości:

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

Wystarczy zastąpić Collectors.toMap()połączenie do połączenia z tą funkcją, a to rozwiąże problem.

Emmanuel Touzery
źródło
1
Ale dopuszczanie nullwartości i używanie putIfAbsentnie działa dobrze razem. Nie wykrywa duplikatów kluczy, gdy są mapowane na null
Holger
10

Tak, późna odpowiedź ode mnie, ale myślę, że może pomóc zrozumieć, co dzieje się pod maską, na wypadek, gdyby ktoś chciał Collectornapisać inną logikę.

Próbowałem rozwiązać problem, kodując bardziej natywne i proste podejście. Myślę, że jest to tak bezpośrednie, jak to możliwe:

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

    };
  }

}

A testy przy użyciu JUnit i 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");
  }

A jak tego używasz? Cóż, po prostu użyj go zamiast toMap()jak pokazują testy. Dzięki temu kod wywołujący wygląda tak czysto, jak to możliwe.

EDYCJA:
zaimplementowano poniżej pomysł Holgera, dodano metodę testową

sjngm
źródło
1
Kombinator nie sprawdza duplikatów kluczy. Jeśli chcesz uniknąć sprawdzania każdego klucza, możesz użyć czegoś takiego(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 Tak, to prawda. Zwłaszcza, że accumulator()faktycznie to sprawdza. Może powinienem kiedyś zrobić kilka równoległych strumieni :)
sjngm
7

Oto nieco prostszy kolektor niż zaproponowany przez @EmmanuelTouzery. Użyj go, jeśli chcesz:

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

Po prostu zamieniamy na nulljakiś niestandardowy obiekt nonei wykonujemy operację odwrotną w finiszerze.

Tagir Valeev
źródło
5

Jeśli wartością jest String, może to działać: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

Gnana
źródło
4
Działa to tylko wtedy, gdy nie masz nic przeciwko modyfikowaniu danych. Metody podrzędne mogą oczekiwać wartości pustych zamiast pustych ciągów.
Sam Buchmiller
3

Według 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)

Kiedy nazywa się map.merge

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

Będzie to zrobić nullczek jako pierwszą rzeczą

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

Nie używam Java 8 tak często, więc nie wiem, czy jest lepszy sposób, aby to naprawić, ale naprawa jest trochę trudna.

Mógłbyś:

Użyj filtru, aby odfiltrować wszystkie wartości NULL, a w kodzie JavaScript sprawdź, czy serwer nie wysłał żadnej odpowiedzi na ten identyfikator, oznacza, że ​​nie odpowiedział na to.

Coś takiego:

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

Lub użyj peek, który służy do zmiany elementu strumienia dla elementu. Za pomocą peek możesz zmienić odpowiedź na coś bardziej akceptowalnego dla mapy, ale oznacza to nieco edycję logiki.

Wygląda na to, że jeśli chcesz zachować obecny projekt, którego powinieneś unikać Collectors.toMap

Marco Acierno
źródło
3

Lekko zmodyfikowałem implementację Emmanuela Touzery'ego .

Ta wersja;

  • Pozwala na klucze zerowe
  • Umożliwia wartości zerowe
  • Wykrywa zduplikowane klucze (nawet jeśli są puste) i zgłasza IllegalStateException, jak w oryginalnej implementacji JDK.
  • Wykrywa zduplikowane klucze również wtedy, gdy klucz jest już zamapowany na wartość zerową. Innymi słowy, oddziela mapowanie wartością zerową od braku mapowania.
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;
        }
    );
}

Testy jednostkowe:

@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))
    );
}
mmdemirbas
źródło
1

Przepraszam, że ponownie otworzyłem stare pytanie, ale ponieważ ostatnio edytowałem, że „problem” nadal występuje w Javie 11, czułem, że chciałbym to podkreślić:

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

daje wyjątek wskaźnika zerowego, ponieważ mapa nie dopuszcza wartości zerowej jako wartości. Ma to sens, ponieważ jeśli spojrzysz na mapę w poszukiwaniu klucza ki nie ma go, to zwrócona wartość jest już null(patrz javadoc). Gdybyś więc mógł wprowadzić ktę wartość null, mapa wyglądałaby tak, jakby zachowywała się dziwnie.

Jak ktoś powiedział w komentarzach, dość łatwo to rozwiązać za pomocą filtrowania:

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

w ten sposób żadne nullwartości nie zostaną wstawione do mapy, a JESZCZE otrzymasz nulljako „wartość”, gdy szukasz identyfikatora, który nie ma odpowiedzi na mapie.

Mam nadzieję, że ma to sens dla wszystkich.

Luca
źródło
1
Byłoby sensowne, gdyby mapa nie dopuszczała wartości zerowych, ale tak jest. Możesz zrobić to answerMap.put(4, null);bez żadnych problemów. Masz rację, że dzięki proponowanemu rozwiązaniu uzyskasz ten sam wynik dla anserMap.get (), jeśli nie jest obecny, jakby wartość została wstawiona jako null. Jeśli jednak powtórzysz wszystkie wpisy na mapie, to oczywiście będzie różnica.
Jasper,
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());
}
Igor Zubchenok
źródło
1
upvoting, ponieważ to się kompiluje. Zaakceptowana odpowiedź nie jest kompilowana, ponieważ Map :: putAll nie ma wartości zwracanej.
Taugenichts
0

Zachowując wszystkie pytania identyfikatory z drobnymi poprawkami

Map<Integer, Boolean> answerMap = 
  answerList.stream()
            .collect(Collectors.toMap(Answer::getId, a -> 
                       Boolean.TRUE.equals(a.getAnswer())));
Sigirisetti
źródło
Myślę, że to najlepsza odpowiedź - to najbardziej zwięzła odpowiedź, która rozwiązuje problem NPE.
LConrad
-3

NullPointerException jest zdecydowanie najczęściej spotykanym wyjątkiem (przynajmniej w moim przypadku). Aby tego uniknąć, wybieram defensywę i dodam kilka zerowych czeków, a ostatecznie mam rozdęty i brzydki kod. Java 8 wprowadza Opcjonalne do obsługi odwołań zerowych, dzięki czemu można zdefiniować wartości zerowalne i zerowalne.

To powiedziawszy, zawinię wszystkie zerowalne odwołania w opcjonalny pojemnik. Nie powinniśmy również łamać kompatybilności wstecznej. Oto kod.

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);
    }
}
TriCore
źródło
1
bezużyteczna odpowiedź, dlaczego warto pozbyć się null, aby to naprawić? Jest to problem z Collectors.toMap()wartościami
zerowymi
@Enerccio uspokój kolego !! Poleganie na wartościach zerowych nie jest dobrą praktyką. Gdybyś użył Opcjonalnego, nie spotkałbyś NPE w pierwszej kolejności. Przeczytaj o Opcjonalnych zastosowaniach.
TriCore
1
a dlaczego to? Wartość null jest w porządku, problemem jest nieudokumentowana biblioteka. Opcjonalne jest fajne, ale nie wszędzie.
Enerccio,