Java 8 NullPointerException dans Collectors.toMap

331

Le Java 8 Collectors.toMaplance un NullPointerExceptionsi l'une des valeurs est 'null'. Je ne comprends pas ce comportement, les cartes peuvent contenir des pointeurs nuls comme valeur sans aucun problème. Y a-t-il une bonne raison pour laquelle les valeurs ne peuvent pas être nulles Collectors.toMap?

De plus, y a-t-il une bonne façon de résoudre ce problème en Java 8, ou devrais-je revenir à la boucle ancienne pour la boucle?

Un exemple de mon problème:

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

Trace de la pile:

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)

Ce problème existe toujours dans Java 11.

Jaspe
la source
5
nulla toujours été un peu problématique, comme dans TreeMap. Peut-être un bon moment pour essayer Optional<Boolean>? Sinon, divisez et utilisez le filtre.
Joop Eggen
5
@JoopEggen nullpourrait être un problème pour une clé, mais dans ce cas, c'est la valeur.
gontard
Toutes les cartes n'ont pas de problème avec null, HashMappar exemple, peuvent avoir une nullclé et un nombre quelconque de nullvaleurs, vous pouvez essayer de créer un personnalisé en Collectorutilisant un HashMapau lieu d'utiliser celui par défaut.
kajacx
2
@kajacx Mais l'implémentation par défaut est HashMap- comme indiqué dans la première ligne de stacktrace. Le problème n'est pas qu'une valeur Mapne peut pas contenir null, mais que le deuxième argument de la Map#mergefonction ne peut pas être nul.
czerny
Personnellement, dans les circonstances données, j'irais avec une solution non stream, ou forEach () si l'entrée est parallèle. Les belles solutions basées sur le flux court ci-dessous pourraient avoir des performances terribles.
Ondra Žižka

Réponses:

302

Vous pouvez contourner ce bogue connu dans OpenJDK avec ceci:

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

Ce n'est pas très joli, mais ça marche. Résultat:

1: true
2: true
3: null

( ce tutoriel m'a le plus aidé.)

kajacx
la source
3
@Jagger oui, une définition d'un fournisseur (le premier argument) est une fonction qui ne transmet aucun paramètre et retourne un résultat, donc le lambda pour votre cas serait () -> new TreeMap<>(String.CASE_INSENSITIVE_ORDER)de créer une Stringclé insensible à la casse TreeMap.
Brett Ryan
2
C'est la bonne réponse, et à mon humble avis ce que le JDK devrait faire pour sa version par défaut non surchargée. Peut-être que la fusion est plus rapide, mais je n'ai pas testé.
Brett Ryan
1
Je devais spécifier les paramètres de type afin de compiler, de cette façon: Map<Integer, Boolean> collect = list.stream().collect(HashMap<Integer, Boolean>::new, (m,v)->m.put(v.getId(), v.getAnswer()), HashMap<Integer, Boolean>::putAll);. J'ai eu: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
Cela peut être assez lent sur une grande entrée. Vous créez un HashMap, puis appelez putAll()pour chaque entrée unique. Personnellement, dans des circonstances données, j'irais avec une solution non stream, ou forEach()si l'entrée est parallèle.
Ondra Žižka
3
Attention, cette solution se comporte différemment de l'implémentation originale de toMap. L'implémentation d'origine détecte les clés en double et lève une exception IllegalStatException, mais cette solution accepte silencieusement la dernière clé. La solution d'Emmanuel Touzery ( stackoverflow.com/a/32648397/471214 ) est plus proche du comportement d'origine.
mmdemirbas
174

Ce n'est pas possible avec les méthodes statiques de Collectors. Le javadoc de toMapexplique qui toMapest basé sur Map.merge:

@param mergeFunction une fonction de fusion, utilisée pour résoudre les collisions entre les valeurs associées à la même clé, telle que fournie à Map#merge(Object, Object, BiFunction)}

et le javadoc Map.mergedit:

@throws NullPointerException si la clé spécifiée est nulle et que cette carte ne prend pas en charge les clés nulles ou si la valeur ou remappingFunction est nulle

Vous pouvez éviter la boucle for en utilisant la forEachméthode de votre liste.

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

mais ce n'est pas vraiment simple que l'ancienne:

Map<Integer, Boolean> answerMap = new HashMap<>();
for (Answer answer : answerList) {
    answerMap.put(answer.getId(), answer.getAnswer());
}
gontard
la source
3
Dans ce cas, je préfère utiliser l'ancien pour chacun. Dois-je considérer cela comme un bug dans toMerge? car l'utilisation de cette fonction de fusion est vraiment un détail d'implémentation, ou est-ce un bon raisonnement pour ne pas autoriser toMap à traiter des valeurs nulles?
Jasper
6
Il est spécifié dans le javadoc de fusion, mais il n'est pas indiqué dans le doc de toMap
Jasper
119
Jamais pensé que les valeurs nulles dans la carte auraient un tel impact sur l'API standard, je préfère le considérer comme une faille.
Askar Kalykov
16
En fait, les documents de l'API n'indiquent rien sur l'utilisation de Map.merge. Cette IMHO est une faille dans la mise en œuvre qui restreint un cas d'utilisation parfaitement acceptable qui a été ignoré. Les méthodes surchargées de toMapdo indiquent l'utilisation de Map.mergemais pas celle que l'OP utilise.
Brett Ryan
11
@Jasper, il y a même un rapport de bogue bugs.openjdk.java.net/browse/JDK-8148463
pixel
23

J'ai écrit un Collectorqui, contrairement à celui par défaut de Java, ne plante pas lorsque vous avez des nullvaleurs:

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

Remplacez simplement votre Collectors.toMap()appel par un appel à cette fonction et cela résoudra le problème.

Emmanuel Touzery
la source
1
Mais autoriser des nullvaleurs et les utiliser putIfAbsentne fonctionnent pas bien ensemble. Il ne détecte pas les clés en double lors de leur mappage sur null
Holger
10

Oui, une réponse tardive de ma part, mais je pense que cela peut aider à comprendre ce qui se passe sous le capot au cas où quelqu'un voudrait coder une autre Collectorlogique.

J'ai essayé de résoudre le problème en codant une approche plus native et simple. Je pense que c'est aussi direct que possible:

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

    };
  }

}

Et les tests utilisant JUnit et 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");
  }

Et comment l'utilisez-vous? Eh bien, utilisez-le plutôt que toMap()comme le montrent les tests. Cela rend le code appelant aussi propre que possible.

EDIT:
implémentation de l'idée de Holger ci-dessous, ajout d'une méthode de test

sjngm
la source
1
Le combineur ne vérifie pas les clés en double. Si vous voulez éviter de vérifier chaque clé, vous pouvez utiliser quelque chose comme(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 Oui, c'est vrai. D'autant plus accumulator()que ça vérifie. Peut-être que je devrais faire quelques streams parallèles une fois :)
sjngm
7

Voici un collecteur un peu plus simple que celui proposé par @EmmanuelTouzery. Utilisez-le si vous le souhaitez:

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

Nous remplaçons simplement nullpar un objet personnalisé noneet faisons l'opération inverse dans le finisseur.

Tagir Valeev
la source
5

Si la valeur est une chaîne, cela peut fonctionner: map.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> Optional.ofNullable(e.getValue()).orElse("")))

Gnana
la source
4
Cela ne fonctionne que si vous êtes d'accord avec la modification des données. Les méthodes en aval peuvent s'attendre à des valeurs nulles plutôt qu'à des chaînes vides.
Sam Buchmiller
3

Selon le 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)

Quand s'appelle le map.merge

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

Il fera un nullchèque comme première chose

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

Je n'utilise pas Java 8 si souvent, donc je ne sais pas s'il existe une meilleure façon de le réparer, mais le réparer est un peu difficile.

Vous pourriez faire:

Utilisez un filtre pour filtrer toutes les valeurs NULL, et dans le code Javascript, vérifiez si le serveur n'a envoyé aucune réponse pour cet identifiant signifie qu'il n'y a pas répondu.

Quelque chose comme ça:

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

Ou utilisez peek, qui est utilisé pour modifier l'élément de flux pour élément. En utilisant Peek, vous pouvez changer la réponse en quelque chose de plus acceptable pour la carte, mais cela signifie modifier un peu votre logique.

On dirait que si vous souhaitez conserver la conception actuelle, vous devez éviter Collectors.toMap

Marco Acierno
la source
3

J'ai légèrement modifié l'implémentation d'Emmanuel Touzery .

Cette version;

  • Autorise les clés nulles
  • Autorise les valeurs nulles
  • Détecte les clés en double (même si elles sont nulles) et lève IllegalStateException comme dans l'implémentation JDK d'origine.
  • Détecte également les clés en double lorsque la clé est déjà mappée à la valeur nulle. En d'autres termes, sépare un mappage avec une valeur nulle de non-mappage.
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;
        }
    );
}

Tests unitaires:

@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
la source
1

Désolé de rouvrir une ancienne question, mais comme elle a été modifiée récemment en disant que le «problème» reste toujours dans Java 11, j'ai eu envie de le souligner:

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

vous donne l'exception de pointeur null car la carte n'autorise pas null comme valeur. Cela a du sens car si vous recherchez une clé dans la carte ket qu'elle n'est pas présente, la valeur renvoyée l'est déjà null(voir javadoc). Donc, si vous pouviez entrer kla valeur null, la carte ressemblerait à un comportement étrange.

Comme quelqu'un l'a dit dans les commentaires, il est assez facile de résoudre ce problème en utilisant le filtrage:

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

de cette façon, aucune nullvaleur ne sera insérée dans la carte, et vous obtiendrez TOUJOURSnull la "valeur" lorsque vous recherchez un identifiant qui n'a pas de réponse dans la carte.

J'espère que cela a du sens pour tout le monde.

Luca
la source
1
Cela aurait du sens si une carte n'autorisait pas les valeurs nulles, mais c'est le cas. Vous pouvez le faire answerMap.put(4, null);sans aucun problème. Vous avez raison: avec votre solution proposée, vous obtiendrez le même résultat pour anserMap.get () s'il n'est pas présent comme si la valeur serait insérée comme nulle. Cependant, si vous parcourez toutes les entrées de la carte, il y a évidemment une différence.
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
la source
1
upvoting parce que cela compile. La réponse acceptée ne se compile pas car Map :: putAll n'a pas de valeur de retour.
Taugenichts
0

Conserver tous les identifiants des questions avec un petit ajustement

Map<Integer, Boolean> answerMap = 
  answerList.stream()
            .collect(Collectors.toMap(Answer::getId, a -> 
                       Boolean.TRUE.equals(a.getAnswer())));
sigirisetti
la source
Je pense que c'est la meilleure réponse - c'est la réponse la plus concise et elle résout le problème NPE.
LConrad
-3

NullPointerException est de loin l'exception la plus fréquemment rencontrée (du moins dans mon cas). Pour éviter cela, je vais sur la défensive et j'ajoute un tas de contrôles nuls et je finis par avoir du code gonflé et laid. Java 8 introduit Facultatif pour gérer les références nulles afin que vous puissiez définir des valeurs nullables et non nullables.

Cela dit, j'envelopperais toutes les références nullables dans le conteneur facultatif. Nous ne devons pas non plus rompre la compatibilité descendante également. Voici le 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);
    }
}
TriCore
la source
1
réponse inutile, pourquoi devriez-vous vous débarrasser de null pour résoudre ce problème? C'est un problème de Collectors.toMap()valeurs non nulles
Enerccio
@Enerccio calme toi mon pote !! S'appuyer sur des valeurs nulles n'est pas une bonne pratique. Si vous aviez utilisé Facultatif, vous n'auriez pas rencontré NPE en premier lieu. Renseignez-vous sur les utilisations facultatives.
TriCore
1
et pourquoi est-ce que? La valeur nulle est très bien, c'est la bibliothèque non documentée qui est le problème. En option, c'est bien mais pas partout.
Enerccio