web-dev-qa-db-fra.com

Java 8 NullPointerException dans Collectors.toMap

Java 8 Collectors.toMap renvoie une NullPointerException si 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 null pour Collectors.toMap?

De plus, existe-t-il une solution de Nice Java 8 permettant de résoudre ce problème ou devrais-je revenir à plain old?

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.

225
Jasper

Ce n'est pas possible avec les méthodes statiques de Collectors. Le javadoc de toMap explique que toMap est 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 de Map.merge dit: 

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

Vous pouvez éviter la boucle for en utilisant la méthode forEach 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 manière:

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

Vous pouvez contourner ce bug 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 beau, mais ça marche. Résultat:

1: true
2: true
3: null

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

177
kajacx

J'ai écrit une Collector qui, contrairement à celle par défaut de Java, ne plante pas lorsque vous avez des valeurs null:

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

Il suffit de remplacer votre appel Collectors.toMap() à un appel de cette fonction et le problème sera résolu.

16
Emmanuel Touzery

Oui, une réponse tardive de ma part, mais je pense qu'il peut être utile de comprendre ce qui se passe sous le capot au cas où quelqu'un voudrait coder une autre logique Collector-.

J'ai essayé de résoudre le problème en codant une approche plus native et directe. 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 (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));
      }

    };
  }

}

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

Et comment l'utilisez-vous? Eh bien, utilisez-le au lieu de toMap(), comme le montrent les tests. Cela rend le code d'appel aussi propre que possible.

7
sjngm

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

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 null par un objet personnalisé none et effectuons l'opération inverse dans le module de finition.

5
Tagir Valeev

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

4
Gnana

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 une vérification null comme première chose

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

Je n'utilise pas souvent Java 8, donc je ne sais pas s'il existe un meilleur moyen de le réparer, mais le réparer est un peu difficile.

Vous pourriez faire:

Utilisez filter pour filtrer toutes les valeurs NULL. Dans le code Javascript, vérifiez si le serveur n'a pas envoyé de réponse car cet identifiant signifie qu'il n'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'élément. En utilisant Peek, vous pouvez changer la réponse en une chose plus acceptable pour la carte, mais cela signifie modifier un peu votre logique.

On dirait que si vous voulez conserver la conception actuelle, vous devriez éviter Collectors.toMap

3
Marco Acierno

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

Cette version;

  • Permet les clés nulles
  • Permet 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 les clés en double également lorsque la clé est déjà associée à la valeur NULL. 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))
    );
}
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

Désolé de rouvrir une vieille question, mais comme elle a été modifiée récemment en disant que le "problème" est toujours présent dans Java 11, je me suis dit que je voulais signaler ceci:

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

vous donne l'exception du pointeur null car la carte n'autorise pas null en tant que valeur . Cela est logique car si vous recherchez dans la carte la clé k et qu'elle n'est pas présente, la valeur renvoyée est déjà null (voir javadoc ). Donc, si vous pouviez mettre dans k la valeur null, la carte aurait l'air de se comporter étrangement.

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 manière, aucune valeur null ne sera insérée dans la carte, et ENCORE, vous obtiendrez null comme "valeur" lorsque vous rechercherez un identifiant qui n'a pas de réponse dans la carte.

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

0
Luca

Conserver toutes les identifiants de questions avec un petit Tweak

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