中间流操作未按计数计算

2022-09-01 03:47:37

似乎我无法理解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());
}

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

当我添加过滤器操作时: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

这个看似冗余的筛选器操作如何更改组合流管道的行为?


答案 1

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

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

如果操作管道中存在操作,则无法再知道最初已知的流的大小(因为可能会拒绝流的某些元素)。因此,不执行块,执行中间操作,从而修改StringBuilder。filter()filterif

另一方面,如果管道中只有元素,则保证流中的元素数与初始元素数相同。因此,执行 if 块,并直接返回大小,而不计算中间操作。map()

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


答案 2

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

副作用的消除也可能令人惊讶。除了 forEach 和 forEachOrdered 的终端操作之外,当流实现可以优化行为参数的执行而不影响计算结果时,行为参数的副作用可能并不总是执行。(有关具体示例,请参阅计数操作上记录的 API 说明。

接口说明:

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

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

流源(List)覆盖的元素数是已知的,中间操作(peek)不会注入流中或从中删除元素(如 flatMap 或筛选器操作)。因此,计数是 List 的大小,并且不需要执行管道,并且作为副作用,打印出列表元素。


推荐