63

在 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 允许使用 null 键,并且 List.class.getSuperclass() 返回 null。但是 Collectors.groupingBy 在 Collectors.java 第 907 行发出 NPE:

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

如果我创建自己的收集器,它将起作用,并将此行更改为:

K key = classifier.apply(t);  

我的问题是:

1) Collectors.groupingBy 的 Javadoc 并没有说它不应该映射一个空键。出于某种原因,这种行为是必要的吗?

2)是否有另一种更简单的方法来接受空键,而不必创建我自己的收集器?

4

7 回答 7

73

我有同样的问题。这失败了,因为 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())));
于 2014-09-26T12:13:20.837 回答
17

对于第一个问题,我同意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 给出了另一个更复杂的答案,它允许您指定自己的地图和列表供应商。我没有测试过。

于 2014-03-26T02:14:03.400 回答
8

分组前使用过滤器##

在 groupingBy 之前过滤掉空实例。

这是一个例子
MyObjectlist.stream()
  .filter(p -> p.getSomeInstance() != null)
  .collect(Collectors.groupingBy(MyObject::getSomeInstance));
于 2015-08-25T15:08:37.737 回答
3

对于您的第一个问题,来自文档:

不保证返回的 Map 或 List 对象的类型、可变性、可序列化性或线程安全性。

因为不是所有的 Map 实现都允许空键,所以他们可能添加了这个来减少映射的最常见的允许定义,以便在选择类型时获得最大的灵活性。

对于您的第二个问题,您只需要一个供应商,lambda 不工作吗?我仍然熟悉Java 8,也许更聪明的人可以添加更好的答案。

于 2014-03-25T04:16:43.943 回答
3

首先,您正在使用大量原始对象。这根本不是一个好主意首先转换以下内容:

  • ClassClass<?>即。而不是原始类型,而是具有未知类的参数化类型。
  • HashMap您应该将 a 提供HashMap给收集器,而不是强制转换为 a 。

首先是正确键入的代码,而不关心 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键,那么它应该被)允许。

到目前为止,我没有看到任何其他更简单的方法,我会尝试更多地研究它。

于 2014-03-25T08:09:57.177 回答
3

我想我会花点时间尝试消化你遇到的这个问题。我整理了一个 SSCE,如果我手动完成,我会期望什么,以及groupingBy实现的实际作用。

我不认为这是一个答案,但它是一个'想知道为什么它是一个问题'的事情。此外,如果您愿意,可以随意破解此代码以获得对 null 友好的收集器。

编辑:通用友好的实现:

/** 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.length() 对 String 值进行分组,(如果输入 String 为 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)
于 2014-03-25T14:35:08.610 回答
0

你可以Stream#collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)改用。

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


当您有一个自定义 POJO 类型的对象列表时:

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

输出:

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

在你的情况下:

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

输出:

null=[interface java.util.List]
class java.util.AbstractList=[class java.util.ArrayList]
于 2021-09-02T08:29:48.867 回答