Java 收集器提供输入列表

And*_*din 7 java functional-programming type-inference java-stream collectors

我正在尝试实现一个简单的收集器,它采用收集器列表,并同时以略有不同的方式从流中收集值。

它与 非常相似Collectors.teeing,但不同之处在于

  1. 收到收藏家列表,而不是仅仅两个
  2. 要求所有收集器产生相同类型的值

我想要的类型签名是

public static <T, R> Collector<T, ?, List<R>> list(
      final List<Collector<T, ?, R>> downstreamCollectors);
Run Code Online (Sandbox Code Playgroud)

创建此类收集器的一种方法是递归地配对发球收集器,如下所示:

public static <T, R> Collector<T, ?, List<R>> list(
    final List<Collector<T, ?, R>> downstreamCollectors) {
  return listrec(
      Collectors.collectingAndThen(downstreamCollectors.get(0), List::of),
      downstreamCollectors.stream().skip(1).toList());
}

private static <T, R> Collector<T, ?, List<R>> listrec(
    final Collector<T, ?, List<R>> teedCollectors,
    final List<Collector<T, ?, R>> downstreamCollectors) {
  if (downstreamCollectors.size() == 0) {
    return teedCollectors;
  } else {
    return listrec(
        teeing(
            teedCollectors,
            downstreamCollectors.get(0),
            (l, s) -> Stream.concat(l.stream(), Stream.of(s)).toList()),
        downstreamCollectors.stream().skip(1).toList());
  }
}
Run Code Online (Sandbox Code Playgroud)

这个解决方案感觉有点“不对劲”,所以我尝试自己创建收集器,例如:

public static <T, R> Collector<T, ?, List<R>> list2(
    final List<Collector<T, ?, R>> downstreamCollectors) {
  return Collector.of(
      () -> downstreamCollectors.stream().map(c -> c.supplier().get()).toList(),
      (accumulators, t) ->
          IntStream.range(0, downstreamCollectors.size())
              .forEach(
                  i -> downstreamCollectors.get(i).accumulator().accept(accumulators.get(i), t)),
      (accumulator1, accumulator2) ->
          IntStream.range(0, downstreamCollectors.size())
              .mapToObj(
                  i ->
                      downstreamCollectors
                          .get(i)
                          .combiner()
                          .apply(accumulator1.get(i), accumulator2.get(i)))
              .toList(),
      accumulators ->
          IntStream.range(0, downstreamCollectors.size())
              .mapToObj(i -> downstreamCollectors.get(i).finisher().apply(accumulators.get(i)))
              .toList());
}
Run Code Online (Sandbox Code Playgroud)

由于下游收集器的累加器类型中存在无界通配符,因此无法编译。将类型签名更改为

public static <T, A, R> Collector<? super T, ?, List<R>> list2(
    final List<Collector<? super T, A, R>> downstreamCollectors);
Run Code Online (Sandbox Code Playgroud)

解决了问题,但不幸的是,该方法的可用性大大降低,因为下游收集器(例如来自 的内置收集器java.util.stream.Collectors)通常在累加器类型中具有无界通配符。

是否有另一种方法可以实现此目的,将通配符保留在方法签名中?

我使用的是 OpenJDK 17.0.2。

Hol*_*ger 7

将具有任意累加器类型的收集器列表处理为平面列表不能以类型安全的方式完成,因为它需要声明n类型变量来捕获这些类型,其中n是实际列表大小。

\n

因此,您只能将处理实现为操作组合,每个操作在编译时都具有有限数量的组件,就像递归方法一样。

\n

这仍然具有简化的潜力,例如替换downstreamCollectors.size() == 0downstreamCollectors.isEmpty()downstreamCollectors.stream().skip(1).toList()为免费复制downstreamCollectors.subList(1, downstreamCollectors.size())

\n

但最大的影响是将递归代码替换为 Stream Reduction 操作:

\n
public static <T, R> Collector<T, ?, List<R>> list(List<Collector<T, ?, R>> collectors) {\n    return collectors.stream()\n            .<Collector<T, ?, List<R>>>map(c-> Collectors.collectingAndThen(c, List::of))\n            .reduce((c1, c2) -> teeing(c1, c2,\n                        (l1, l2) -> Stream.concat(l1.stream(), l2.stream()).toList()))\n            .orElseThrow(() -> new IllegalArgumentException("no collector specified"));\n}\n
Run Code Online (Sandbox Code Playgroud)\n

如果您没有\xe2\x80\x99t 需要组成大量的收集器,这可能会相当有效。这种简洁解决方案的缺点是,在实际合并结果之前,每个结果都将被包装到单个元素列表中,甚至结果合并可能会承担多个列表复制操作。

\n

可以使用以下方法优化该结果处理

\n
public static <T, R> Collector<T, ?, List<R>> list(List<Collector<T, ?, R>> collectors) {\n    int num = collectors.size();\n    switch(num) {\n        case 0: throw new IllegalArgumentException("no collector specified");\n        case 1: return collectingAndThen(collectors.get(0), List::of);\n        case 2: return teeing(collectors.get(0), collectors.get(1), List::of);\n        case 3: return teeing(teeing(collectors.get(0), collectors.get(1), List::of),\n                           collectors.get(2), (l,r) -> List.of(l.get(0), l.get(1), r));\n        default:\n    }\n    Collector<T,?,List<R>> c = teeing(collectors.get(0), collectors.get(1), (r1, r2) -> {\n        var list = new ArrayList<R>(num);\n        list.add(r1);\n        list.add(r2);\n        return list;\n    });\n    for(int ix = 2; ix < num; ix ++) {\n        c = teeing(c, collectors.get(ix), (list, r) -> { list.add(r); return list; });\n    }\n    return collectingAndThen(c, List::copyOf);\n}\n
Run Code Online (Sandbox Code Playgroud)\n

这为少量收集器提供了特殊情况,其结果可用于直接构造不可变结果列表。对于其他情况,ArrayList在将列表转换为最终的不可变列表之前,所有结果都会添加到第一个,以防止过度的列表复制。如果获取不可变的结果列表并不重要,那么最后一步可以省略,我只是尝试尽可能接近Stream.toList()原始方法的行为。

\n

在流处理过程中,\xe2\x80\x99s 在幕后仍然存在不平衡的递归结构,这阻止了真正大量的收集器。有两种方法可以解决这个问题。

\n
    \n
  1. 实现您自己的类型安全变体,它公开中间容器类型,以允许构建平衡树并通过遍历该树将所有结果收集到列表中,而无需额外的中间存储。

    \n
  2. \n
  3. 放弃类型安全并使用平面列表和原始类型实现收集器。尽量限制不安全的代码。

    \n
  4. \n
\n

但是,当您估计了 \xe2\x80\x9ctee\xe2\x80\x9d 的收集器的预期数量并发现第一个解决方案工作得足够好时,可能不需要这样做。

\n