未按计数评估中间流操作

ata*_*tus 36 java java-stream

我似乎无法理解 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());
}
Run Code Online (Sandbox Code Playgroud)

控制台只打印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());
}
Run Code Online (Sandbox Code Playgroud)

输出更改为:

4
1234
Run Code Online (Sandbox Code Playgroud)

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

JB *_*zet 40

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

if (StreamOpFlag.SIZED.isKnown(helper.getStreamAndOpFlags()))
    return spliterator.getExactSizeIfKnown();
return super.evaluateSequential(helper, spliterator);
Run Code Online (Sandbox Code Playgroud)

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

另一方面,如果您只有map()在管道中,则流中的元素数量保证与初始元素数量相同。所以执行了if块,直接返回size,不求中间操作。

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

  • 关于 flatMap,我不这么认为。AFAIK,因为一开始让它变得渴望更简单。是的,使用流和 map() 来产生副作用是一个坏主意。 (3认同)

Dea*_*ool 19

jdk-9 中,它清楚地记录在 java docs 中

消除副作用也可能令人惊讶。除了 forEach 和 forEachOrdered 的终端操作之外,当流实现可以在不影响计算结果的情况下优化掉行为参数的执行时,行为参数的副作用可能并不总是被执行。(有关特定示例,请参阅有关计数操作的 API 说明。)

API注意事项:

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

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

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