中级流操作未按计数进行评估


33

似乎我很难理解Java如何将流操作组合到流管道中。

执行以下代码时

public
 static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

控制台仅打印4。该StringBuilder对象仍然具有价值""

当我添加过滤器操作时: filter(s -> true)

public static void main(String[] args) {
    StringBuilder sb = new StringBuilder();

    var count = Stream.of(new String[]{"1", "2", "3", "4"})
            .filter(s -> true)
            .map(sb::append)
            .count();

    System.out.println(count);
    System.out.println(sb.toString());
}

输出更改为:

4
1234

这种看似多余的过滤器操作如何改变合成流管道的行为?


2
有趣的!
uneq95

3
我可以想象这是特定于实现的行为;可能是因为第一个流的大小已知,但是第二个流的大小未知,而size-ness决定是否执行中间操作。
安迪·特纳

出于兴趣,如果反转过滤器和映射会发生什么?
安迪·特纳

在Haskell中进行了一些编程后,它闻起来有点像在进行一些懒惰的评估。谷歌搜索返回,该流确实有些懒惰。可能是这样吗?如果没有过滤器,那么如果Java很聪明,则无需实际执行映射。
Frederik

@AndyTurner即使在逆转时,它也提供相同的结果
uneq95

Answers:


39

count()终端操作,在我的版本的JDK,结束执行以下代码:

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);

如果filter()操作流水线中有一个操作,则无法再知道最初已知的流大小(因为filter可能会拒绝该流的某些元素)。因此,if不执行该块,执行中间操作,并因此修改StringBuilder。

另一方面,如果仅map()在管道中,则流中的元素数保证与初始元素数相同。因此,如果执行了if块,则不评估中间操作就直接返回大小。

请注意,传递给的lambda map()违反了文档中定义的约定:它应该是无干扰的无状态操作,但并非无状态。因此,在两种情况下得出不同的结果都不能视为错误。


因为flatMap()也许可以更改元素的数量,这是它最初渴望(现在很懒)的原因吗?因此,我猜想,forEach()如果map()以当前形式违反合同,替代方法是分别使用和计数。
弗雷德里克

3
关于flatMap,我不这么认为。之所以如此,是因为AFAIK,因为它的起步很简单,所以很热心。是的,使用带有map()的流来产生副作用是一个坏主意。
JB Nizet

您是否对如何在4 1234不利用额外的滤波器或在map()操作中产生副作用的情况下实现完整输出提出了建议 ?
atalantus 16:02

1
int count = array.length; String result = String.join("", array);
JB Nizet

1
或者,如果您确实想使用StringBuilder,则可以使用forEach,也可以使用Collectors.joining("")
njzk2

19

jdk-9中,它清楚地记录在Java文档中

消除副作用也可能令人惊讶。除了终端操作forEach和forEachOrdered之外,当流实现可以优化行为参数的执行而不影响计算结果时,行为参数的副作用可能不会始终执行。(有关特定示例,请参见计数操作中记录的API注释。)

API注意:

如果实现能够直接从流源计算计数,则实现可以选择不执行流管道(顺序地或并行地)。在这种情况下,将不会遍历任何源元素,也不会评估任何中间操作。强烈建议避免带有副作用的行为参数,除了无害情况(例如调试)外。例如,考虑以下流:

 List<String> l = Arrays.asList("A", "B", "C", "D");
 long count = l.stream().peek(System.out::println).count();

流源所覆盖的元素数量(即List)是已知的,并且中间操作peek不会注入或删除流中的元素(对于flatMap或filter操作可能就是这种情况)。因此,计数就是List的大小,并且不需要执行管道,并且副作用是打印出list元素。


0

这不是.map的目的。应该使用它来将“ Something”流转换为“ Something Else”流。在这种情况下,您将使用map将字符串追加到外部Stringbuilder,之后您将获得一个“ Stringbuilder”流,每个流都是由map操作创建的,该操作将一个数字附加到原​​始Stringbuilder上。

您的流实际上对流中的映射结果不执行任何操作,因此假设该步骤可以被流处理器跳过是完全合理的。您要依靠副作用来完成这项工作,这会破坏地图的功能模型。使用forEach可以更好地为您服务。完全将计数作为一个单独的流进行,或者使用forEach中的AtomicInt放置一个计数器。

过滤器强制它运行流内容,因为它现在必须对每个流元素做一些在概念上有意义的事情。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.