Подтвердить что ты не робот

Collectors.groupingBy не принимает нулевые ключи

В Java 8 это работает:

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

Но это не так:

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

Карты допускают нулевой ключ, а List.class.getSuperclass() возвращает значение null. Но Collectors.groupingBy испускает NPE, на Collectors.java, строка 907:

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

Он работает, если я создаю свой собственный сборщик, с этой строкой изменился на:

K key = classifier.apply(t);  

Мои вопросы:

1) Javadoc of Collectors.groupingBy не говорит, что он не должен отображать нулевой ключ. По какой-то причине это поведение необходимо?

2) Есть ли другой, более простой способ принять нулевой ключ, не создавая собственный коллекционер?

4b9b3361

Ответ 1

По первому вопросу, я согласен с skiwi, что он не должен бросать NPE. Я надеюсь, что они изменят это (или, по крайней мере, добавят его в javadoc). Между тем, чтобы ответить на второй вопрос, я решил использовать Collectors.toMap вместо 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;
        }));

Или, инкапсулируя его:

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

И используйте его следующим образом:

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

Обратите внимание, что rolfl дал еще один, более сложный ответ, который позволяет вам указать своего собственного поставщика карт и списков. Я его не тестировал.

Ответ 2

У меня была такая же проблема. Это не удалось, поскольку groupingBy выполняет Objects.requireNonNull по значению, возвращаемому из классификатора:

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

Используя опцию, это работает:

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

Ответ 3

Прежде всего, вы используете много необработанных объектов. Это не очень хорошая идея, сначала конвертируйте следующее:

  • Class до Class<?>, т.е. вместо необработанного типа, параметризованный тип с неизвестным классом.
  • Вместо принудительного нажатия на HashMap, вы должны поставить HashMap в коллекционер.

Сначала правильно напечатанный код, не заботясь о NPE:

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

Теперь мы избавляемся от силового броска там, а вместо этого делаем это правильно:

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

Здесь мы заменяем groupingBy, который просто берет классификатор, тот, который принимает классификатор, поставщик и коллекционер. По сути, это то же самое, что и раньше, но теперь оно правильно напечатано.

Вы действительно правы, что в javadoc не указано, что он будет бросать NPE, и я не думаю, что это должно быть выбрасывание одного из них, поскольку я могу предоставить любую карту, которую я хочу, и если моя карта позволяет использовать клавиши null, тогда это должно быть разрешено.

Я не вижу другого способа сделать это проще на данный момент, я постараюсь больше в него поработать.

Ответ 4

Использовать фильтр перед группировкойBy

Отфильтруйте нулевые экземпляры перед группировкойBy.

Вот пример
MyObjectlist.stream().filter(p -> p.getSomeInstance() != null).collect(Collectors.groupingBy(MyObject::getSomeInstance));

Ответ 5

К вашему 1-му вопросу, из документов:

Нет никаких гарантий относительно типа, изменчивости, сериализуемости или безопасности потоков возвращаемых объектов Map или List.

Поскольку не все реализации Map допускают пустые ключи, они, вероятно, добавили это, чтобы уменьшить до наиболее распространенного допустимого определения карты, чтобы получить максимальную гибкость при выборе типа.

К вашему второму вопросу, вам просто нужен поставщик, не будет ли лямбда работать? Я все еще знакомлюсь с Java 8, возможно, более умный человек может добавить лучший ответ.

Ответ 6

Я решил, что я займу минутку и попытаюсь переварить эту проблему. Я собрал SSCE для того, чего бы я ожидал, если бы я сделал это вручную, и что действительно реализует реализация groupingBy.

Я не думаю, что это ответ, но это "интересно, почему это проблема". Кроме того, если вы хотите, не стесняйтесь взломать этот код, чтобы иметь нулевой коллекционер.

Изменить: универсальная реализация:

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

Рассмотрим этот код (коды групп String на основе String.length(), (или null, если входная строка равна NULL)):

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

Вышеприведенный код выводит результат:

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