Java 8 NullPointerException em Collectors.toMap

331

O Java 8 Collectors.toMaplança a NullPointerExceptionse um dos valores for 'nulo'. Eu não entendo esse comportamento, os mapas podem conter ponteiros nulos como valor sem problemas. Existe uma boa razão pela qual os valores não podem ser nulos Collectors.toMap?

Além disso, existe uma maneira agradável do Java 8 de corrigir isso, ou devo reverter para o antigo loop for?

Um exemplo do meu problema:

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)

Esse problema ainda existe no Java 11.

Jaspe
fonte
5
nullsempre foi um pouco problemático, como no TreeMap. Talvez um bom momento para experimentar Optional<Boolean>? Caso contrário, divida e use o filtro.
Joop Eggen
5
@JoopEggen nullpode ser um problema para uma chave, mas, neste caso, é o valor.
Gontard
Nem todos os mapas têm problemas null, HashMappor exemplo, podem ter uma nullchave e qualquer número de nullvalores, você pode tentar criar um personalizado Collectorusando a em HashMapvez de usar o padrão.
kajacx
2
@kajacx Mas a implementação padrão é HashMap- como mostrado na primeira linha do stacktrace. O problema não é que um valor Mapnão pode conter null, mas que o segundo argumento da Map#mergefunção não pode ser nulo.
Czerny
Pessoalmente, nas circunstâncias dadas, eu usaria uma solução sem fluxo ou forEach () se a entrada for paralela. As boas soluções baseadas em fluxo curto abaixo podem ter um desempenho terrível.
Ondra Žižka

Respostas:

302

Você pode contornar isso bug conhecido no OpenJDK com isso:

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

Não é tão bonito, mas funciona. Resultado:

1: true
2: true
3: null

( este tutorial me ajudou mais.)

kajacx
fonte
3
@ Jagger sim, uma definição de fornecedor (o primeiro argumento) é uma função que não passa parâmetros e retorna um resultado; portanto, o lambda para o seu caso seria () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)criar uma Stringchave sem distinção entre maiúsculas e minúsculas TreeMap.
Brett Ryan
2
Esta é a resposta correta, e IMHO o que o JDK deveria estar fazendo para sua versão padrão não sobrecarregada. Talvez a mesclagem seja mais rápida, mas eu não testei.
Brett Ryan
1
Eu tive que especificar os parâmetros de tipo, a fim de compilar, de que maneira: Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);. Eu tinha: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
Isso pode ser bastante lento em uma entrada grande. Você cria um HashMape depois chama putAll()para cada entrada. Pessoalmente, em determinadas circunstâncias, eu usaria uma solução sem fluxo ou forEach()se a entrada for paralela.
Ondra Žižka
3
Lembre-se de que esta solução se comporta de maneira diferente da implementação original do toMap. A implementação original detecta chaves duplicadas e lança uma IllegalStatException, mas esta solução aceita silenciosamente a chave mais recente. A solução de Emmanuel Touzery ( stackoverflow.com/a/32648397/471214 ) está mais próxima do comportamento original.
Mmdemirbas 08/08/19
174

Não é possível com os métodos estáticos de Collectors. O javadoc de toMapexplica que toMapé baseado em Map.merge:

@param mergeFunction uma função de mesclagem, usada para resolver colisões entre valores associados à mesma chave, conforme fornecido para Map#merge(Object, Object, BiFunction)}

e o javadoc de Map.mergediz:

@throws NullPointerException se a chave especificada for nula e este mapa não suportar chaves nulas ou o valor ou remappingFunction for nulo

Você pode evitar o loop for usando o forEachmétodo da sua lista.

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

mas não é realmente simples do que o jeito antigo:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}
gontard
fonte
3
Nesse caso, prefiro usar o antiquado para cada um. Devo considerar isso um bug no toMerge? como o uso dessa função de mesclagem é realmente um detalhe de implementação ou é um bom motivo para não permitir que o toMap processe valores nulos?
Jasper
6
Ele é especificado no javadoc de mesclagem, mas não é declarado no documento do toMap
Jasper
119
Nunca pensei que valores nulos no mapa causariam tanto impacto na API padrão, prefiro considerá-la uma falha.
Askar Kalykov
16
Na verdade, os documentos da API não afirmam nada sobre o uso de Map.merge. Esse IMHO é uma falha na implementação que restringe um caso de uso perfeitamente aceitável que foi ignorado. Os métodos sobrecarregados de toMapafirmam o uso, Map.mergemas não o que o OP está usando.
Brett Ryan
11
@ Jasper existe até um relatório de bug bugs.openjdk.java.net/browse/JDK-8148463
pixel
23

Eu escrevi um Collectorque, diferente do java padrão, não falha quando você tem nullvalores:

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

Basta substituir sua Collectors.toMap()chamada por uma chamada para esta função e isso resolverá o problema.

Emmanuel Touzery
fonte
1
Mas permitir nullvalores e usar putIfAbsentnão funciona bem juntos. Ele não detecta chaves duplicadas quando eles são mapeados para null...
Holger
10

Sim, uma resposta tardia minha, mas acho que pode ajudar a entender o que está acontecendo sob o capô, caso alguém queira codificar alguma outra Collectorlógica.

Tentei resolver o problema codificando uma abordagem mais nativa e direta. Eu acho que é o mais direto possível:

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

    };
  }

}

E os testes usando JUnit e 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");
  }

E como você usa isso? Bem, basta usá-lo em vez detoMap() como mostram os testes. Isso faz com que o código de chamada pareça o mais limpo possível.

EDIT:
implementou a ideia de Holger abaixo, adicionou um método de teste

sjngm
fonte
1
O combinador não verifica se há chaves duplicadas. Se você deseja evitar a verificação de todas as chaves, pode usar algo como(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 Sim, isso é verdade. Especialmente porque accumulator()realmente verifica isso. Talvez eu devesse fazer algumas correntes paralelas, uma vez :)
sjngm
7

Aqui está um colecionador um pouco mais simples do que o proposto por @EmmanuelTouzery. Use-o se quiser:

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

Apenas substituímos nullpor algum objeto personalizado nonee fazemos a operação inversa no finalizador.

Tagir Valeev
fonte
5

Se o valor for uma String, isso poderá funcionar: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

Gnana
fonte
4
Isso funciona apenas se você estiver bem com a modificação dos dados. Os métodos downstream podem esperar valores nulos em vez de cadeias vazias.
Sam Buchmiller 13/03/19
3

De acordo com 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)

Quando é chamado de map.merge

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

Ele fará uma nullverificação como primeira coisa

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

Eu não uso o Java 8 com tanta frequência, então não sei se há uma maneira melhor de corrigi-lo, mas corrigi-lo é um pouco difícil.

Você poderia fazer:

Use filter para filtrar todos os valores NULL e, no código Javascript, verifique se o servidor não enviou nenhuma resposta para esse ID significa que ele não respondeu.

Algo assim:

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

Ou use peek, que é usado para alterar o elemento de fluxo para o elemento. Usando o peek, você pode alterar a resposta para algo mais aceitável para o mapa, mas isso significa editar um pouco sua lógica.

Parece que, se você quiser manter o design atual, evite Collectors.toMap

Marco Acierno
fonte
3

Eu ligeiramente modificada implementação do Emmanuel Touzery .

Esta versão;

  • Permite chaves nulas
  • Permite valores nulos
  • Detecta chaves duplicadas (mesmo que sejam nulas) e lança IllegalStateException como na implementação original do JDK.
  • Detecta chaves duplicadas também quando a chave já está mapeada para o valor nulo. Em outras palavras, separa um mapeamento com valor nulo e sem mapeamento.
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;
        }
    );
}

Testes unitários:

@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
fonte
1

Desculpe reabrir uma pergunta antiga, mas desde que ela foi editada recentemente dizendo que o "problema" ainda permanece no Java 11, senti que queria apontar isso:

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

fornece a exceção do ponteiro nulo porque o mapa não permite nulo como um valor. Isso faz sentido, porque se você procurar a chave em um mapa ke ela não estiver presente, o valor retornado já estará null(consulte javadoc). Portanto, se você pudesse colocar ko valor null, o mapa pareceria estar se comportando de maneira estranha.

Como alguém disse nos comentários, é muito fácil resolver isso usando a filtragem:

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

dessa forma, nenhum nullvalor será inserido no mapa e AINDA você obteránull o "valor" ao procurar um ID que não tenha resposta no mapa.

Espero que isso faça sentido para todos.

Luca
fonte
1
Faria sentido se um mapa não permitisse valores nulos, mas permite. Você pode fazer answerMap.put(4, null);sem problemas. Você está certo que, com a solução proposta, obterá o mesmo resultado para anserMap.get () se ele não estiver presente, como se o valor fosse inserido como nulo. No entanto, se você iterar sobre todas as entradas do mapa, obviamente haverá uma diferença.
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
fonte
1
voto positivo porque isso compila. A resposta aceita não é compilada porque Map :: putAll não possui um valor de retorno.
Taugenichts
0

Reter todos os IDs de perguntas com pequenos ajustes

Map<Integer, Boolean> answerMap = 
  answerList.stream()
            .collect(Collectors.toMap(Answer::getId, a -> 
                       Boolean.TRUE.equals(a.getAnswer())));
sigirisetti
fonte
Eu acho que essa é a melhor resposta - é a resposta mais concisa e corrige o problema do NPE.
LConrad
-3

NullPointerException é de longe a exceção mais frequentemente encontrada (pelo menos no meu caso). Para evitar isso, fico na defensiva e adiciono várias verificações nulas e acabo tendo um código inchado e feio. O Java 8 introduz o Opcional para manipular referências nulas, para que você possa definir valores nulos e não nulos.

Dito isto, gostaria de agrupar todas as referências anuláveis ​​no contêiner opcional. Também não devemos quebrar a compatibilidade com versões anteriores. Aqui está o código.

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
fonte
1
resposta inútil, por que você deve se livrar de null para corrigir isso? Isso é problema de Collectors.toMap()valores não nulos
Enerccio
@Enerccio acalme-se amigo !! Confiar em valores nulos não é uma boa prática. Se você tivesse usado o Opcional, não teria encontrado o NPE em primeiro lugar. Leia sobre usos opcionais.
TriCore
1
e por que isto? Valor nulo é bom, é a biblioteca não documentada que é o problema. Opcional é bom, mas não em todos os lugares.
Enerccio