Коллекционеры.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));

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

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

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

K key = classifier.apply(t);  

мои вопросы:

1) Javadoc коллекторов.groupingBy не говорит, что он не должен сопоставлять нулевой ключ. Это поведение по какой-то причине?

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

6 ответов


у меня была такая же проблема. Это не удалось, поскольку groupingBy выполняет объекты.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())));

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

  • Class to Class<?>, ie. вместо необработанного типа-параметризованный тип с неизвестным классом.
  • вместо того, чтобы насильно бросать в 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 ключи, то это должно быть разрешено.

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


для первого вопроса я согласен со скиви, что он не должен бросать 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 дал другой, более сложный ответ, который позволяет вам указать свою собственную карту и список поставщиков. Я его не проверял.


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

нет никаких гарантий типа, изменяемости, сериализации или потокобезопасности возвращаемых объектов карты или списка.

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

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


Я подумал, что возьму минутку и попытаюсь переварить этот вопрос, который у вас есть. Я собрал 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);
}

рассмотреть этот код (код группирует строковые значения на основе строки.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$groupingBy5(Collectors.java:907)
  at java.util.stream.Collectors$$Lambda/258952499.accept(Unknown Source)
  at java.util.stream.ReduceOpsReducingSink.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)

используйте фильтр перед groupingBy

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

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