Java Collectors.groupingBy是否可以将Stream作为其分组项目列表返回?

Eri*_*ikE 5 java grouping java-stream collectors

在C#的LINQ,GroupBy返回IEnumerableIGrouping项目,而这又是一个IEnumerable所选择的值的类型的项目。这是一个例子:

var namesAndScores = new Dictionary<string, int>> {
    ["David"] = 90,
    ["Jane"] = 91,
    ["Bill"] = 90,
    ["Tina"] = 89)
};
var IEnumerable<IGrouping<int, string>> namesGroupedByScore =
    namesAndScores
        .GroupBy(
            kvp => kvp.Value,
            kvp => kvp.Key
        );

// Result:
// 90 : { David, Bill }
// 91 : { Jane }
// 89 : { Tina }
Run Code Online (Sandbox Code Playgroud)

具体来说,请注意,每个IGrouping<int, string>都是IEnumerable<string>和不是List<string>。(它也有一个.Key属性。)

GroupBy显然有但是完全列举输入项目,才可以发出一个分组,因为它发出IEnumerable<string>,而不是一个List<string>,可能有性能优势,如果你不枚举整个分组,比如,如果你只是做.First()

撇开:从技术上讲,我想GroupBy可以等到您枚举它从输入中消耗单个项,然后发出一个IGrouping,并且仅在枚举时枚举其余的输入,在IGrouping搜索时将其他组收集到其内部数据结构中对于当前组中的下一个项目,但是我发现这是一个不太可能且有问题的实现,并且希望GroupBy在调用时能够完全枚举。

这是带有以下代码的代码First()

 var oneStudentForEachNumericScore = namesGroupedByScore
     .ToDictionary(
         grouping => grouping.Key,
         grouping => grouping.First() // does not fully enumerate the values
     );
 // Result:
 // 90 : David -- Bill is missing and we don't care
 // 91 : Jane
 // 89 : Tina
Run Code Online (Sandbox Code Playgroud)

现在在Java Streams中,要进行分组,就必须收集,而不能仅仅给groupingBy收集器第二个lambda来提取值。如果您想要的值与整个输入的值不同,则必须再次映射(尽管请注意,groupingBy收集器使您可以一步一步地创建多级分组或多组分组)。这是与上述C#代码等效的代码:

Map<Integer, List<String>> namesGroupedByScore = namesAndScores
      .entrySet().stream()
      .collect(Collectors.groupingBy(
          Map.Entry::getValue,
          Collectors.mapping(
              Map.Entry::getKey,
              Collectors.toList(),
          )
      ));
Run Code Online (Sandbox Code Playgroud)

这似乎不是最佳选择。所以我的问题是:

  1. 有什么方法可以更简单地表达这一点,而不必使用Collectors.mapping使组项目成为值?
  2. 为什么我们必须收集为完全枚举的类型?有没有一种方法可以模拟IEnumerableC#的值类型GroupByMap<Integer, Stream<String>>从中返回a Collectors.mapping(),或者这是没有用的,因为无论如何必须对值项进行完全枚举?还是我们可以编写自己Collectors.groupingBy的代码作为第二个参数的lambda并为我们完成工作,使语法更接近Linq的GroupBy语法,并且至少具有更简洁的语法并可能稍微改善性能?
  3. 从理论上讲,即使实际上没有用处,也可以编写我们自己的Java Stream Collector toStream(),该Java Stream Collector 返回a Stream,并且直到被枚举(除非一次迭代一个元素,递归)才对输入进行迭代?

Hol*_*ger 5

虽然这些操作在某些方面看起来相似,但它们本质上是不同的。与 Linq\xe2\x80\x99sGroupBy操作不同,Java\xe2\x80\x99sgroupingBy是一个,旨在与 Stream API 的终端操作配合Collector使用,它本身不是中间操作,因此可以\xe2\x80\ x99t 通常用于实现惰性流操作。 collect

\n\n

收集groupingBy器使用组的另一个下游Collector,因此在最好的情况下,您可以指定一个收集器就地执行该操作,而不是流式传输 group\xe2\x80\x99s 元素来执行其他操作。虽然这些收集器不支持短路,但它们不需要将组收集到Lists 中,只需对它们进行流式传输即可。只要考虑一下,例如groupingBy(f1, summingInt(f2))。将组收集到a中的情况List被认为足够常见,足以使toList()在您不指定收集器时暗示,但是在收集到之前将元素映射到的情况还没有考虑到这一点。一个列表。

\n\n

如果您经常遇到这种情况,那么定义您自己的收集器就会很容易

\n\n
public static <T,K,V> Collector<T,?,Map<K,List<V>>> groupingBy(\n    Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {\n    return Collectors.groupingBy(key, Collectors.mapping(value, Collectors.toList()));\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

并像这样使用它

\n\n
Map<Integer,List<String>> result = map.entrySet().stream()\n    .collect(groupingBy(Map.Entry::getValue, Map.Entry::getKey));\n
Run Code Online (Sandbox Code Playgroud)\n\n

并且,由于您不需要使用方法引用并且希望更接近 Linq 原始版本:

\n\n
Map<Integer,List<String>> result = map.entrySet().stream()\n        .collect(groupingBy(kvp -> kvp.getValue(), kvp -> kvp.getKey()));\n
Run Code Online (Sandbox Code Playgroud)\n\n

但是,如上所述,如果您稍后要流式传输此地图并担心此操作的非惰性,那么您可能想使用不同的收集器toList()

\n\n

虽然这种方法在结果值方面提供了一定的灵活性,但Map及其键是该操作不可避免的一部分,因为 不仅提供Map存储逻辑,其查找操作还负责形成组,这也决定了语义。例如,当您与地图供应商一起使用变体() -> new TreeMap<>(customComparator)时,您可能会得到与默认组完全不同的组HashMap(想想,例如String.CASE_INSENSITIVE_ORDER)。另一方面,当您提供 时EnumMap,您可能不会获得不同的语义,但会获得完全不同的性能特征。

\n\n

相比之下,GroupBy您描述的 Linq 操作看起来像是一个中间操作,在 Stream API 中根本没有挂件。正如您自己所建议的,当第一个元素被轮询时,它仍然会进行完整的遍历,从而在幕后完全填充数据结构。即使实现尝试一些懒惰,结果也是有限的。您可以便宜地获得第一组的第一个元素,但如果您只对该元素感兴趣,则根本不需要分组。第一组的第二个元素可能已经是源流的最后一个元素,需要完整的遍历和存储。

\n\n

因此,提供这样的操作意味着一定的复杂性,与急切地收集相比几乎没有什么好处。它\xe2\x80\x99s也很难想象它的并行能力实现(提供优于collect操作的好处)。实际的不便并非源于此设计决策,而是源于结果Map不是 aCollectionIterable (请注意,单独实现不会\xe2\x80\x99t 意味着有一个stream()方法)以及分离集合操作和流操作的决定。这两个方面导致需要使用entrySet().stream()流式传输地图,但是 \xe2\x80\x99s 超出了这个问题的范围。而且,如上所述,如果您需要这个,请首先检查是否有不同的下游收集器groupingBy器的不同下游收集器是否无法\xe2\x80\x99t 提供所需的结果。

\n\n

为了完整起见,这里是一个尝试实现惰性分组的解决方案:

\n\n
public interface Group<K,V> {\n    K key();\n    Stream<V> values();\n}\npublic static <T,K,V> Stream<Group<K,V>> group(Stream<T> s,\n    Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {\n\n    return StreamSupport.stream(new Spliterator<Group<K,V>>() {\n        final Spliterator<T> sp = s.spliterator();\n        final Map<K,GroupImpl<T,K,V>> map = new HashMap<>();\n        ArrayDeque<Group<K,V>> pendingGroup = new ArrayDeque<>();\n        Consumer<T> c;\n        {\n        c = t -> map.compute(key.apply(t), (k,g) -> {\n            V v = value.apply(t);\n            if(g == null) pendingGroup.addLast(g = new GroupImpl<>(k, v, sp, c));\n            else g.add(v);\n            return g;\n        });\n        }\n        public boolean tryAdvance(Consumer<? super Group<K,V>> action) {\n            do {} while(sp.tryAdvance(c) && pendingGroup.isEmpty());\n            Group<K,V> g = pendingGroup.pollFirst();\n            if(g == null) return false;\n            action.accept(g);\n            return true;\n        }\n        public Spliterator<Group<K,V>> trySplit() {\n            return null; // that surely doesn\'t work in parallel\n        }\n        public long estimateSize() {\n            return sp.estimateSize();\n        }\n        public int characteristics() {\n            return ORDERED|NONNULL;\n        }\n    }, false);\n}\nstatic class GroupImpl<T,K,V> implements Group<K,V> {\n    private final K key;\n    private final V first;\n    private final Spliterator<T> source;\n    private final Consumer<T> sourceConsumer;\n    private List<V> values;\n\n    GroupImpl(K k, V firstValue, Spliterator<T> s, Consumer<T> c) {\n        key = k;\n        first = firstValue;\n        source = s;\n        sourceConsumer = c;\n    }\n    public K key() {\n        return key;\n    }\n    public Stream<V> values() {\n        return StreamSupport.stream(\n            new Spliterators.AbstractSpliterator<V>(1, Spliterator.ORDERED) {\n            int pos;\n            public boolean tryAdvance(Consumer<? super V> action) {\n                if(pos == 0) {\n                    pos++;\n                    action.accept(first);\n                    return true;\n                }\n                do {} while((values==null || values.size()<pos)\n                           &&source.tryAdvance(sourceConsumer));\n                if(values==null || values.size()<pos) return false;\n                action.accept(values.get(pos++ -1));\n                return true;\n            }\n        }, false);\n    }\n    void add(V value) {\n        if(values == null) values = new ArrayList<>();\n        values.add(value);\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

您可以使用以下示例进行测试:

\n\n
group(\n    Stream.of("foo", "bar", "baz", "hello", "world", "a", "b", "c")\n          .peek(s -> System.out.println("source traversal: "+s)),\n        String::length,\n        String::toUpperCase)\n    .filter(h -> h.values().anyMatch(s -> s.startsWith("B")))\n    .findFirst()\n    .ifPresent(g -> System.out.println("group with key "+g.key()));\n
Run Code Online (Sandbox Code Playgroud)\n\n

这将打印:

\n\n
public static <T,K,V> Collector<T,?,Map<K,List<V>>> groupingBy(\n    Function<? super T, ? extends K> key, Function<? super T, ? extends V> value) {\n    return Collectors.groupingBy(key, Collectors.mapping(value, Collectors.toList()));\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

表明惰性尽可能发挥作用。但

\n\n
    \n
  • 每个需要知道所有组/键的操作都需要完全遍历源,因为最后一个元素可能会引入一个新组
  • \n
  • 每个需要处理至少一个组的所有元素的操作都需要完全遍历,因为源的最后一个元素可能属于该组
  • \n
  • 上一点甚至适用于短路操作,如果它们可以\xe2\x80\x99t 提前停止。例如,在上面的示例中,在第二组中找到匹配项意味着对第一组的完全遍历不成功,因此对源的完全遍历
  • \n
  • 上面的例子可以重写为

    \n\n
    Stream.of("foo", "bar", "baz", "hello", "world", "a", "b", "c")\n      .peek(s -> System.out.println("source traversal: "+s))\n      .filter(s -> s.toUpperCase().startsWith("H"))\n      .map(String::length)\n      .findFirst()\n      .ifPresent(key -> System.out.println("group with key "+key));\n
    Run Code Online (Sandbox Code Playgroud)\n\n

    这提供了更好的惰性(例如,如果匹配不在第一组内)。

    \n\n

    当然,这个例子是人为的,但我有一种强烈的感觉,几乎任何具有惰性处理潜力的操作,即不需要所有组并且不需要至少一个组的所有元素,都可以重写为一个操作根本不需要分组。

  • \n
\n