【问题标题】:Collectors.groupingBy doesn't accept null keysCollectors.groupingBy 不接受空键
【发布时间】:2014-05-02 17:37:19
【问题描述】:

在 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) 是否有另一种更简单的方法来接受空键,而无需创建自己的收集器?

【问题讨论】:

标签: java hashmap java-8 java-stream collectors


【解决方案1】:

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

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

使用 Optional,这可行:

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

【讨论】:

    【解决方案2】:

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

    【讨论】:

    • 谢谢,它帮助我满足了不同的需求:groupBy 和 Count,这是我的包装:public static &lt;T,R&gt; Map&lt;R,Long&gt; countGroupBy(Collection&lt;T&gt; list, Function&lt;T,R&gt; groupBy){ Collector&lt;T, ?, Map&lt;R, Long&gt;&gt; mapCollectors = Collectors.toMap( groupBy, l-&gt;1L, Long::sum); return list.stream().collect(mapCollectors); }
    【解决方案3】:

    groupingBy##前使用过滤器

    在 groupingBy 之前过滤掉空实例。

    这是一个例子
    MyObjectlist.stream()
      .filter(p -> p.getSomeInstance() != null)
      .collect(Collectors.groupingBy(MyObject::getSomeInstance));
    

    【讨论】:

    • OPS 的第二个问题暗示他们想要接受空键,所以这不能回答他的问题。
    • 这可能是正确答案的一部分。另一部分是您可以在结果中显式放置 null 项:result.put(null, myList.stream.filter(p -&gt; p.get() == null).collect(Collectors.toList());Kind 有点丑,但可能仍然比创建收集器更方便。
    • 问题:groupingBy lambda 函数必须被调用两次。这可能很昂贵,具体取决于功能
    【解决方案4】:

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

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

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

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

    【讨论】:

    • 您可以指定一个接受空键的地图应用程序,所以是的,它从不接受空键是一个很大的问题。
    • Jason,Map 接口允许它接受或拒绝空值。因此,如果他们想要最大的灵活性,他们应该接受它们。无论如何,如果有人使用不接受空值的地图实现,它可能会抛出自己的异常。
    • 接口支持,但实现并不都支持空键。通过防止通用收集器中的空值,他们可以灵活地选择任何地图实现。我同意这感觉不对,但很可能他们权衡了基于性能调整(或无论如何选择实现)与最广泛(如与 Map 接口的所有实现一起使用)键定义的神秘空指针异常的可能性。
    【解决方案5】:

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

    • ClassClass&lt;?&gt;,即。而不是原始类型,而是具有未知类的参数化类型。
    • 您应该向收集器提供 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 替换为接受分类器、供应商和收集器的groupingBy。本质上,这与以前的相同,但现在输入正确。

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

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

    【讨论】:

    • Skiwi,我打了一个不好的例子,因为我真的不需要HashMap,而且更简单的groupingBy 不能保证返回一个。我的例子应该只是 Map,像这样:Stream&lt;Class&lt;?&gt;&gt; stream = Stream.of(ArrayList.class);Map&lt;Class&lt;?&gt;, List&lt;Class&lt;?&gt;&gt;&gt; map = stream.collect(Collectors.groupingBy(Class::getSuperclass));
    • 对 IntellijIDEA13 用户的警告:IDE 在 Class::getSuperclass 上发出 Cyclic inference 错误。如果您删除所有&lt;?&gt;,则不会显示此错误。但是现在我意识到即使出现这个“错误”它也能编译和运行,所以它似乎不是一个真正的错误,而是一个 IntellijIDEA 错误。
    • 这个答案不起作用。 Collectors 中的所有 groupingBy 方法最终在内部使用相同的实现,该实现拒绝空键。不敢相信在投票之前没有人费心检查这个......
    【解决方案6】:

    我想我会花点时间尝试消化您遇到的这个问题。我整理了一个 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)
    

    【讨论】:

      【解决方案7】:

      您可以改用Stream#collect(Supplier&lt;R&gt; supplier, BiConsumer&lt;R,? super T&gt; accumulator, BiConsumer&lt;R,R&gt; 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]
      

      【讨论】:

        猜你喜欢
        • 2019-06-28
        • 2019-11-13
        • 2015-05-27
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-12-26
        • 1970-01-01
        相关资源
        最近更新 更多