web-dev-qa-db-fra.com

Collectors.groupingBy n'accepte pas les clés nulles

En Java 8, cela fonctionne:

Stream<Class> stream = Stream.of(ArrayList.class);
HashMap<Class, List<Class>> map = (HashMap)stream.collect(Collectors.groupingBy(Class::getSuperclass));

Mais cela ne veut pas:

Stream<Class> stream = Stream.of(List.class);
HashMap<Class, List<Class>> map = (HashMap)stream.collect(Collectors.groupingBy(Class::getSuperclass));

Maps autorise une clé nulle et List.class.getSuperclass () renvoie null. Mais Collectors.groupingBy émet un NPE, à Collectors.Java, ligne 907:

K key = Objects.requireNonNull(classifier.apply(t), "element cannot be mapped to a null key"); 

Cela fonctionne si je crée mon propre collecteur, avec cette ligne remplacée par:

K key = classifier.apply(t);  

Mes questions sont:

1) Le Javadoc de Collectors.groupingBy ne dit pas qu'il ne doit pas mapper une clé nulle. Ce comportement est-il nécessaire pour une raison quelconque?

2) Existe-t-il un autre moyen, plus simple, d'accepter une clé nulle, sans avoir à créer mon propre collecteur?

37
MarcG

Pour la première question, je suis d'accord avec skiwi qu'il ne devrait pas lancer un NPE. J'espère qu'ils vont changer cela (ou bien au moins l'ajouter au javadoc). En attendant, pour répondre à la deuxième question, j'ai décidé d'utiliser Collectors.toMap au lieu de Collectors.groupingBy:

Stream<Class<?>> stream = Stream.of(ArrayList.class);

Map<Class<?>, List<Class<?>>> map = stream.collect(
    Collectors.toMap(
        Class::getSuperclass,
        Collections::singletonList,
        (List<Class<?>> oldList, List<Class<?>> newEl) -> {
        List<Class<?>> newList = new ArrayList<>(oldList.size() + 1);
        newList.addAll(oldList);
        newList.addAll(newEl);
        return newList;
        }));

Ou, en l'encapsulant:

/** Like Collectors.groupingBy, but accepts null keys. */
public static <T, A> Collector<T, ?, Map<A, List<T>>>
groupingBy_WithNullKeys(Function<? super T, ? extends A> classifier) {
    return Collectors.toMap(
        classifier,
        Collections::singletonList,
        (List<T> oldList, List<T> newEl) -> {
            List<T> newList = new ArrayList<>(oldList.size() + 1);
            newList.addAll(oldList);
            newList.addAll(newEl);
            return newList;
            });
    }

Et utilisez-le comme ceci:

Stream<Class<?>> stream = Stream.of(ArrayList.class);
Map<Class<?>, List<Class<?>>> map = stream.collect(groupingBy_WithNullKeys(Class::getSuperclass));

Veuillez noter que rolfl a donné une autre réponse, plus compliquée, qui vous permet de spécifier votre propre fournisseur de cartes et de listes. Je ne l'ai pas testé.

9
MarcG

J'ai eu le même genre de problème. Cela a échoué, car groupingBy effectue Objects.requireNonNull sur la valeur renvoyée par le classificateur:

    Map<Long, List<ClaimEvent>> map = events.stream()
      .filter(event -> eventTypeIds.contains(event.getClaimEventTypeId()))
      .collect(groupingBy(ClaimEvent::getSubprocessId));

En utilisant facultatif, cela fonctionne:

    Map<Optional<Long>, List<ClaimEvent>> map = events.stream()
      .filter(event -> eventTypeIds.contains(event.getClaimEventTypeId()))
      .collect(groupingBy(event -> Optional.ofNullable(event.getSubprocessId())));
41
Erling

Utiliser le filtre avant de grouper

Filtrez les instances nulles avant groupingBy.

MyObjectlist.stream().filter(p -> p.getSomeInstance() != null).collect(Collectors.groupingBy(MyObject::getSomeInstance));
6
Ilan M

Tout d'abord, vous utilisez beaucoup d'objets bruts. Ce n'est pas une bonne idée pas du tout, convertissez d'abord ce qui suit:

  • Class à Class<?>, c'est à dire. au lieu d'un type brut, un type paramétré avec une classe inconnue.
  • Au lieu d'effectuer une conversion forcée vers un HashMap, vous devez fournir un HashMap au collecteur.

D'abord le code correctement tapé, sans se soucier encore d'un NPE:

Stream<Class<?>> stream = Stream.of(ArrayList.class);
HashMap<Class<?>, List<Class<?>>> hashMap = (HashMap<Class<?>, List<Class<?>>>)stream
        .collect(Collectors.groupingBy(Class::getSuperclass));

Maintenant, nous nous débarrassons de la distribution puissante, et au lieu de cela, faisons-le correctement:

Stream<Class<?>> stream = Stream.of(ArrayList.class);
HashMap<Class<?>, List<Class<?>>> hashMap = stream
        .collect(Collectors.groupingBy(
                Class::getSuperclass,
                HashMap::new,
                Collectors.toList()
        ));

Ici, nous remplaçons le groupingBy qui prend juste un classificateur, par un qui prend un classificateur, un fournisseur et un collecteur. Essentiellement, c'est la même chose qu'avant, mais maintenant il est correctement tapé.

Vous avez en effet raison de dire que dans le javadoc, il n'est pas indiqué qu'il lancera un NPE, et je ne pense pas qu'il devrait en lancer un, car je suis autorisé à fournir la carte que je veux, et si ma carte autorise les touches null, alors il doit être autorisé.

Je ne vois pas d'autre moyen de faire plus simple pour l'instant, j'essaierai de m'y intéresser davantage.

5
skiwi

À votre 1ère question, de la documentation:

Il n'y a aucune garantie sur le type, la mutabilité, la sérialisation ou la sécurité des threads des objets Map ou List retournés.

Parce que toutes les implémentations de carte n'autorisent pas les clés nulles, elles ont probablement ajouté cela pour réduire à la définition autorisée la plus courante d'une carte pour obtenir une flexibilité maximale lors du choix d'un type.

Pour votre 2e question, vous avez juste besoin d'un fournisseur, un lambda ne fonctionnerait-il pas? Je me familiarise toujours avec Java 8, peut-être qu'une personne plus intelligente peut ajouter une meilleure réponse.

3
Jason Sperske

J'ai pensé que je prendrais un moment pour essayer de digérer ce problème. J'ai rassemblé un SSCE pour ce à quoi je m'attendrais si je le faisais manuellement, et ce que fait réellement l'implémentation groupingBy.

Je ne pense pas que ce soit une réponse, mais c'est une question "me demande pourquoi c'est un problème". Aussi, si vous le souhaitez, n'hésitez pas à pirater ce code pour avoir un collecteur null-friendly.

Edit: Une implémentation générique:

/** groupingByNF - NullFriendly - allows you to specify your own Map and List supplier. */
private static final <T,K> Collector<T,?,Map<K,List<T>>> groupingByNF (
        final Supplier<Map<K,List<T>>> mapsupplier,
        final Supplier<List<T>> listsupplier,
        final Function<? super T,? extends K> classifier) {

    BiConsumer<Map<K,List<T>>, T> combiner = (m, v) -> {
        K key = classifier.apply(v);
        List<T> store = m.get(key);
        if (store == null) {
            store = listsupplier.get();
            m.put(key, store);
        }
        store.add(v);
    };

    BinaryOperator<Map<K, List<T>>> finalizer = (left, right) -> {
        for (Map.Entry<K, List<T>> me : right.entrySet()) {
            List<T> target = left.get(me.getKey());
            if (target == null) {
                left.put(me.getKey(), me.getValue());
            } else {
                target.addAll(me.getValue());
            }
        }
        return left;
    };

    return Collector.of(mapsupplier, combiner, finalizer);

}

/** groupingByNF - NullFriendly - otherwise similar to Java8 Collections.groupingBy */
private static final <T,K> Collector<T,?,Map<K,List<T>>> groupingByNF (Function<? super T,? extends K> classifier) {
    return groupingByNF(HashMap::new, ArrayList::new, classifier);
}

Considérez ce code (le code regroupe les valeurs de chaîne en fonction de String.length (), (ou null si la chaîne d'entrée est nulle)):

public static void main(String[] args) {

    String[] input = {"a", "a", "", null, "b", "ab"};

    // How we group the Strings
    final Function<String, Integer> classifier = (a) -> {return a != null ? Integer.valueOf(a.length()) : null;};

    // Manual implementation of a combiner that accumulates a string value based on the classifier.
    // no special handling of null key values.
    BiConsumer<Map<Integer,List<String>>, String> combiner = (m, v) -> {
        Integer key = classifier.apply(v);
        List<String> store = m.get(key);
        if (store == null) {
            store = new ArrayList<String>();
            m.put(key, store);
        }
        store.add(v);
    };

    // The finalizer merges two maps together (right into left)
    // no special handling of null key values.
    BinaryOperator<Map<Integer, List<String>>> finalizer = (left, right) -> {
        for (Map.Entry<Integer, List<String>> me : right.entrySet()) {
            List<String> target = left.get(me.getKey());
            if (target == null) {
                left.put(me.getKey(), me.getValue());
            } else {
                target.addAll(me.getValue());
            }
        }
        return left;
    };

    // Using a manual collector
    Map<Integer, List<String>> manual = Arrays.stream(input).collect(Collector.of(HashMap::new, combiner, finalizer));

    System.out.println(manual);

    // using the groupingBy collector.        
    Collector<String, ?, Map<Integer, List<String>>> collector = Collectors.groupingBy(classifier);

    Map<Integer, List<String>> result = Arrays.stream(input).collect(collector);

    System.out.println(result);
}

Le code ci-dessus produit la sortie:

{0=[], null=[null], 1=[a, a, b], 2=[ab]}
Exception in thread "main" Java.lang.NullPointerException: element cannot be mapped to a null key
  at Java.util.Objects.requireNonNull(Objects.Java:228)
  at Java.util.stream.Collectors.lambda$groupingBy$135(Collectors.Java:907)
  at Java.util.stream.Collectors$$Lambda$10/258952499.accept(Unknown Source)
  at Java.util.stream.ReduceOps$3ReducingSink.accept(ReduceOps.Java:169)
  at Java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.Java:948)
  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 CollectGroupByNull.main(CollectGroupByNull.Java:49)
3
rolfl