Java Collectors.groupingBy 不接受空键

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/22625065/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-13 16:56:06  来源:igfitidea点击:

Collectors.groupingBy doesn't accept null keys

javahashmapjava-8java-streamcollectors

提问by MarcG

In Java 8, this works:

在 Java 8 中,这有效:

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

But this doesn't:

但这不会:

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

Maps allows a null key, and List.class.getSuperclass() returns null. But Collectors.groupingBy emits a NPE, at Collectors.java, line 907:

Maps 允许空键,而 List.class.getSuperclass() 返回空值。但是 Collectors.groupingBy 在 Collectors.java 的第 907 行发出 NPE:

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

It works if I create my own collector, with this line changed to:

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

K key = classifier.apply(t);  

My questions are:

我的问题是:

1) The Javadoc of Collectors.groupingBy doesn't say it shouldn't map a null key. Is this behavior necessary for some reason?

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

2) Is there another, easier way, to accept a null key, without having to create my own collector?

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

采纳答案by MarcG

For the first question, I agree with skiwi that it shouldn't be throwing a NPE. I hope they will change that (or else at least add it to the javadoc). Meanwhile, to answer the second question I decided to use Collectors.toMapinstead of Collectors.groupingBy:

对于第一个问题,我同意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;
        }));

Or, encapsulating it:

或者,封装它:

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

And use it like this:

并像这样使用它:

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

Please note rolfl gave another, more complicated answer, which allows you to specify your own Map and List supplier. I haven't tested it.

请注意 rolfl 给出了另一个更复杂的答案,它允许您指定自己的地图和列表供应商。我没有测试过。

回答by Jason Sperske

To your 1st question, from the docs:

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

There are no guarantees on the type, mutability, serializability, or thread-safety of the Map or List objects returned.

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

Because not all Map implementations allow null keys they probably added this to reduce to the most common allowable definition of a map to get maximum flexibility when choosing a type.

因为并非所有 Map 实现都允许空键,所以他们可能会添加它以减少映射的最常见的允许定义,从而在选择类型时获得最大的灵活性。

To your 2nd question, you just need a supplier, wouldn't a lambda work? I'm still getting acquainted with Java 8, maybe a smarter person can add a better answer.

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

回答by skiwi

First of all, you are using lots of raw objects. This is not a good idea at all, first convert the following:

首先,您使用了大量原始对象。这不是一个好主意,在所有的,先转换如下:

  • Classto Class<?>, ie. instead of a raw type, a parametrized type with an unknown class.
  • Instead of forcefully casting to a HashMap, you should supply a HashMapto the collector.
  • ClassClass<?>,即。而不是原始类型,而是具有未知类的参数化类型。
  • HashMap您应该将 a提供HashMap给收集器,而不是强制转换为 a 。

First the correctly typed code, without caring about a NPE yet:

首先是正确键入的代码,而不关心 NPE:

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

Now we get rid of the forceful cast there, and instead do it correctly:

现在我们摆脱了那里的强制转换,而是正确地做:

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

Here we replace the groupingBywhich just takes a classifier, to one that takes a classifier, a supplier and a collector. Essentially this is the same as what there was before, but now it is correctly typed.

在这里,我们将groupingBy仅采用分类器的替换为采用分类器、供应商和收集器的 。本质上这与之前的相同,但现在输入正确。

You are indeed correct that in the javadoc it is not stated that it will throw a NPE, and I do not think it should be throwing one, as I am allowed to supply whatever map I want, and if my map allows nullkeys, then it should be allowed.

您确实是正确的,在 javadoc 中没有说明它会抛出一个NPE,我认为它不应该抛出一个,因为我可以提供我想要的任何地图,如果我的地图允许null键,那么它应该被)允许。

I do not see any other way to do it simpler as of now, I'll try to look more into it.

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

回答by rolfl

I figured I would take a moment and try to digest this issue you have. I put together a SSCE for what I would expect if I did it manually, and what the groupingByimplementation actually does.

我想我会花点时间尝试消化您遇到的这个问题。我把一个 SSCE 放在一起,如果我手动完成,我会期望什么,以及groupingBy实现实际上做了什么。

I don't think this is an answer, but it is a 'wonder why it is a problem' thing. Also, if you want, feel free to hack this code to have a null-friendly collector.

我不认为这是一个答案,但这是一个“奇怪为什么它是一个问题”的事情。此外,如果您愿意,可以随意修改此代码以获得一个对空值友好的收集器。

Edit: A generic-friendly implementation:

编辑:通用友好的实现:

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

Consider this code (the code groups String values based on the String.length(), (or null if the input String is null)):

考虑以下代码(代码根据 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);
}

The above code produces the output:

上面的代码产生输出:

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

回答by Erling

I had the same kind of problem. This failed, because groupingBy performs Objects.requireNonNull on the value returned from the classifier:

我有同样的问题。这失败了,因为 groupingBy 对分类器返回的值执行 Objects.requireNonNull :

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

Using Optional, this works:

使用可选,这有效:

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

回答by Ilan M

Use filter before groupingBy

在 groupingBy 之前使用过滤器

Filter out the null instances before groupingBy.

在 groupingBy 之前过滤掉空实例。

这是一个例子

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