Java 8流:计算进入终端操作的所有元素数量

20

我想知道除了以下方式之外,是否有更好的(或者其他)方法来获取流终止操作中所有项目的计数:

Stream<T> stream = ... // given as parameter
AtomicLong count = new AtomicLong();
stream.filter(...).map(...)
      .peek(t -> count.incrementAndGet())

在那个阶段,count.get() 给出了处理过的项目的实际计数。

我故意跳过了终端操作,因为它可能在 .forEach, .reduce.collect 之间变化。我确实知道.count,但似乎只有在我用 .map 替换 .forEach 并将 .count 作为终端操作时才有效。但我觉得这样使用 .map 是错误的。

我不太喜欢上面的解决方案的原因是:如果在它之后添加一个过滤器,它只会计算特定阶段的元素数量,而不是进入终端操作的元素数量。

我想到的另一种方法是将过滤和映射后的值收集到列表中,并在该列表上进行操作,然后只需调用 list.size() 即可获得计数。但是,如果流的收集导致错误,这种方法将无法工作,而上面的解决方案可以在适当的 try/catch 放置的情况下对迄今处理过的所有项目进行计数。不过这不是硬性要求。


评论不适合进行长时间的讨论;此对话已被移至聊天室 - Bhargav Rao
2个回答

9
似乎在终端操作之前,您已经通过peek拥有了最干净的解决方案。我认为唯一需要这样做的原因是出于调试目的--如果是这种情况,那么peek就是为此而设计的。为此包装流并提供单独的实现过于复杂,而且会浪费大量时间,并导致对添加到流中的所有内容的后续支持。
至于“如果添加了其他过滤器该怎么办?”这部分,可以提供一个代码注释(我们中的许多人都这样做)和一些否则将失败的示例测试用例。
以上为我的个人意见。

0

最好的想法是在自身上使用映射,同时计算映射例程的调用次数。

steam.map(object -> {counter.incrementAndGet(); return object;});

由于这个lambda表达式可以被重复使用,而且你可以用一个对象替换任何lambda表达式,所以你可以创建一个计数器对象,如下:

class StreamCounter<T> implements Function<? super T,? extends T> {
  int counter = 0;
  public T apply(T object) { counter++; return object;}
  public int get() { return counter;}
}

所以使用:

StreamCounter<String> myCounter = new ...;
stream.map(myCounter)...
int count = myCounter.get();

由于再次调用地图是重用地图方法的另一点,因此可以通过扩展流并包装普通流来提供地图方法。

这样,您就可以创建类似以下内容的东西:

AtomicLong myValue = new AtomicLong();
...
convert(stream).measure(myValue).map(...).measure(mySecondValue).filter(...).measure(myThirdValue).toList(...);

这样,您可以简单地拥有自己的流包装器,它在其自己的版本中透明地包装每个流(这不会产生性能或内存开销),并测量任何此类测量点的基数。

在创建映射/减少解决方案时分析算法复杂性时,经常会这样做。通过不仅为计数采用原子长实例,而只采用测量点名称来扩展您的流实现,您的流实现可以容纳无限数量的测量点,同时提供灵活的打印报告方式。

这样的实现可以记住流方法的具体序列以及每个测量点的位置,并带来像:

list ->  (32k)map -> (32k)filter -> (5k)map -> avg(). 

这样的流实现只需编写一次,便可用于测试和报告。

将其构建到日常实现中,可以收集某些处理的统计信息,并通过使用不同操作排列的动态优化来实现。例如,这将是一个查询优化器。

因此,在您的情况下,最好首先重用 StreamCounter,根据使用频率、计数器数量和对DRY原则的亲和力,最终再实现更复杂的解决方案。

PS:StreamCounter 使用 int 值,不是线程安全的,因此在并行流设置中,应该将 int 替换为 AtomicInteger 实例。


9
滥用map而不使用peek没有任何优势。 - Holger
4
抱歉,我不知道“to infuse actual mapping logic by stacking it on top”是什么意思。如果您有“实际映射逻辑”,只需使用 map。如果您没有,请勿链接 map 操作。那会影响计数的 peek 操作吗? - Holger
6
peek 的作用是帮助调试。测试也属于这个类别。与此相反,如果滥用 peek 用于其他目的,则会遇到与滥用 map 相同的问题。就像传递给 peek 的操作一样,传递给 map 的函数可能会以任意顺序和并发方式进行评估,有时甚至根本不会执行。无论您使用 parallelStream().map(funcWithSideEffect).findAny() 还是 parallelStream().peek(actionPerformingSideEffect).findAny(),都没有区别。 - Holger
再次使用peek将不会有任何收益。我们在很多事情上都有这个映射解决方案,甚至在生产中用于提供优化和报告统计信息。如果没有任何问题,我不会自信地使用peek,特别是当映射可以完成同样的工作时。 - Martin Kersten
1
“不出问题就行”这句话与声称滥用 map 而非使用 peek 有优势是完全不同的陈述。缺点完全相同,只是 peek 是为此设计的,但你可以在代码中做任何想做的事情,并让自己惊喜未来会带来什么,但将其宣传为比 OP 已经拥有的更好的解决方案,则是一种不同的行为。 - Holger
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接