Collectors.groupingBy akzeptiert keine Nullschlüssel

Lesezeit: 11 Minuten

Benutzer-Avatar
MarcG

In Java 8 funktioniert dies:

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

Aber das geht nicht:

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

Maps lässt einen Nullschlüssel zu und List.class.getSuperclass() gibt null zurück. Aber Collectors.groupingBy gibt ein NPE aus, bei Collectors.java, Zeile 907:

K key = Objects.requireNonNull(classifier.apply

Es funktioniert, wenn ich meinen eigenen Collector erstelle, wobei diese Zeile geändert wird in:

K key = classifier.apply

Meine Fragen sind:

1) Das Javadoc von Collectors.groupingBy sagt nicht, dass es keinen Nullschlüssel abbilden sollte. Ist dieses Verhalten aus irgendeinem Grund notwendig?

2) Gibt es eine andere, einfachere Möglichkeit, einen Nullschlüssel zu akzeptieren, ohne einen eigenen Kollektor erstellen zu müssen?

  • verwandt: stackoverflow.com/questions/24630963/…

    – Bernie

    13. Juni 2016 um 19:47 Uhr

Ich hatte das gleiche Problem. Dies ist fehlgeschlagen, da groupingBy Objects.requireNonNull für den vom Klassifikator zurückgegebenen Wert ausführt:

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

Mit Optional funktioniert das:

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

Benutzer-Avatar
MarcG

Bei der ersten Frage stimme ich skiwi zu, dass es nicht werfen sollte NPE. Ich hoffe, sie werden das ändern (oder es zumindest zum Javadoc hinzufügen). In der Zwischenzeit habe ich mich zur Beantwortung der zweiten Frage entschieden Collectors.toMap Anstatt von 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;
        }));

Oder es gekapselt:

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

Und benutze es so:

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

Bitte beachten Sie, dass rolfl eine andere, kompliziertere Antwort gegeben hat, mit der Sie Ihren eigenen Karten- und Listenlieferanten angeben können. Ich habe es nicht getestet.

  • Danke, es hat mir bei einem anderen Bedürfnis geholfen: groupBy und Count, hier ist mein Wrapper: public static <T,R> Map<R,Long> countGroupBy(Collection<T> list, Function<T,R> groupBy){ Collector<T, ?, Map<R, Long>> mapCollectors = Collectors.toMap( groupBy, l->1L, Long::sum); return list.stream().collect(mapCollectors); }

    – pdem

    1. Juli 2016 um 13:15 Uhr


Benutzer-Avatar
Ilan M

Verwenden Sie den Filter vor der Gruppierung nach ##

Filtern Sie die Nullinstanzen vor groupingBy heraus.

Hier ist ein Beispiel

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

  • Die zweite Frage des OPS besagt implizit, dass sie Nullschlüssel akzeptieren möchten, sodass dies seine Frage nicht beantwortet.

    – Clint Eastwood

    25. Juli 2016 um 4:20 Uhr

  • Dies kann Teil einer richtigen Antwort sein. Der andere Teil ist, dass Sie dann explizit Null-Elemente in das Ergebnis einfügen können: result.put(null, myList.stream.filter(p -> p.get() == null).collect(Collectors.toList());Irgendwie hässlich, aber vielleicht immer noch bequemer als das Erstellen eines Sammlers.

    – Speedstone

    14. November 2017 um 14:30 Uhr


  • Das Problem: Die Lambda-Funktion groupingBy muss zweimal aufgerufen werden. das kann je nach Funktion teuer werden

    – Daniel Alder

    31. August 2020 um 18:00 Uhr

  • Das andere Problem ist das groupingBy Es ist nicht garantiert, dass das Ergebnis veränderlich ist. Aber du kannst bestehen mapFactory Parameter und liefert eine veränderliche Karte (wie HashMap::new)

    – Kirill Gagarski

    6. Februar um 15:07 Uhr

Zu Ihrer ersten Frage aus den Dokumenten:

Es gibt keine Garantien für Typ, Veränderlichkeit, Serialisierbarkeit oder Threadsicherheit der zurückgegebenen Map- oder List-Objekte.

Da nicht alle Map-Implementierungen Nullschlüssel zulassen, haben sie dies wahrscheinlich hinzugefügt, um sie auf die am häufigsten zulässige Definition einer Map zu reduzieren und maximale Flexibilität bei der Auswahl eines Typs zu erhalten.

Zu Ihrer 2. Frage, Sie brauchen nur einen Lieferanten, würde ein Lambda nicht funktionieren? Ich mache mich immer noch mit Java 8 vertraut, vielleicht kann eine klügere Person eine bessere Antwort hinzufügen.

Benutzer-Avatar
rollfl

Ich dachte, ich würde mir einen Moment Zeit nehmen und versuchen, dieses Problem, das Sie haben, zu verdauen. Ich habe ein SSCE für das zusammengestellt, was ich erwarten würde, wenn ich es manuell machen würde, und was die groupingBy Umsetzung tut es tatsächlich.

Ich denke nicht, dass dies eine Antwort ist, aber es ist eine Frage, warum es ein Problem ist. Wenn Sie möchten, können Sie diesen Code auch hacken, um einen nullfreundlichen Kollektor zu erhalten.

Bearbeiten: Eine generische freundliche Implementierung:

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

Betrachten Sie diesen Code (der Code gruppiert String-Werte basierend auf String.length() (oder null, wenn der Eingabe-String null ist)):

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

Der obige Code erzeugt die Ausgabe:

{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)

Benutzer-Avatar
skiwi

Zunächst einmal verwenden Sie viele Rohobjekte. Das ist keine gute Idee überhauptkonvertieren Sie zuerst Folgendes:

  • Class zu Class<?>, dh. Anstelle eines Rohtyps ein parametrisierter Typ mit einer unbekannten Klasse.
  • Anstatt gewaltsam auf a zu werfen HashMapsollten Sie a angeben HashMap zum Sammler.

Zuerst der richtig getippte Code, ohne sich noch um eine NPE zu kümmern:

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

Jetzt werden wir die kraftvolle Besetzung dort los und machen es stattdessen richtig:

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

Hier ersetzen wir die groupingBy die nur einen Klassifikator nimmt, zu einem, der einen Klassifikator, einen Lieferanten und einen Sammler nimmt. Im Wesentlichen ist dies dasselbe wie zuvor, aber jetzt ist es korrekt eingegeben.

Sie haben in der Tat Recht, dass im Javadoc nicht angegeben ist, dass es einen werfen wird NPEund ich denke nicht, dass es eine werfen sollte, da ich jede Karte liefern darf, die ich will, und wenn meine Karte es zulässt null Schlüssel, dann sollte es erlaubt sein.

Ich sehe derzeit keine andere Möglichkeit, es einfacher zu machen, ich werde versuchen, mich eingehender damit zu befassen.

Benutzer-Avatar
schwarzr1234

Sie können verwenden Stream#collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner) stattdessen.

https://docs.oracle.com/javase/8/docs/api/java/util/stream/Stream.html#collect-java.util.function.Supplier-java.util.function.BiConsumer-java.util. Funktion.BiConsumer-


Wenn Sie eine Liste mit Objekten eines selbstdefinierten POJO-Typs haben:

package code;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;
import static lombok.AccessLevel.PRIVATE;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import lombok.Data;
import lombok.experimental.Accessors;
import lombok.experimental.FieldDefaults;

public class MainGroupListIntoMap {

    public static void main(String[] args) throws Exception {

        final List<Item> items = Arrays.asList(
            new Item().setName("One").setType("1"),
            new Item().setName("Two").setType("1"),
            new Item().setName("Three").setType("1"),
            new Item().setName("Four").setType("2"),
            new Item().setName("Same").setType(null),
            new Item().setName("Same").setType(null),
            new Item().setName(null).setType(null)
        );

        final Map<String, List<Item>> grouped = items
                .stream()
                .collect(HashMap::new,
                         (m, v) -> m.merge(v.getType(),
                                           asList(v),
                                           (oldList, newList) -> Stream.concat(oldList.stream(),
                                                                               newList.stream())
                                                                       .collect(toList())),
                         HashMap::putAll);

        grouped.entrySet().forEach(System.out::println);
    }
}

@Data
@Accessors(chain = true)
@FieldDefaults(level = PRIVATE)
class Item {
    String name;
    String type;
}

Ausgabe:

null=[Item(name=Same, type=null), Item(name=Same, type=null), Item(name=null, type=null)]
1=[Item(name=One, type=1), Item(name=Two, type=1), Item(name=Three, type=1)]
2=[Item(name=Four, type=2)]

In Ihrem Fall:

package code;

import static java.util.Arrays.asList;
import static java.util.stream.Collectors.toList;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

public class MainGroupListIntoMap2 {

    public static void main(String[] args) throws Exception {

        group(asList(ArrayList.class, List.class))
            .entrySet()
            .forEach(System.out::println);
    }

    private static Map<Class<?>, List<Class<?>>> group(List<Class<?>> classes) {
        final Map<Class<?>, List<Class<?>>> grouped = classes
                .stream()
                .collect(HashMap::new,
                         (m, v) -> m.merge(v.getSuperclass(),
                                           asList(v),
                                           (oldList, newList) -> Stream.concat(oldList.stream(),
                                                                               newList.stream())
                                                                       .collect(toList())),
                         HashMap::putAll);

        return grouped;
    }
}

Ausgabe:

null=[interface java.util.List]
class java.util.AbstractList=[class java.util.ArrayList]

1110680cookie-checkCollectors.groupingBy akzeptiert keine Nullschlüssel

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy