可以跳过 Stream.peek() 进行优化

Evg*_*aev 30 java side-effects java-8 java-stream sonarqube

我在 Sonar 中发现了一条规则:

与其他中间 Stream 操作的一个关键区别是 Stream 实现可以自由地跳过调用以peek()达到优化目的。这可能会导致peek()意外地仅调用 Stream 中的部分元素或不调用任何元素。

另外,Javadoc中也提到了这一点:

此方法的存在主要是为了支持调试,您希望在元素流经管道中的某个点时查看它们

什么情况下可以java.util.Stream.peek()跳过?和调试有关系吗?

Nik*_*las 27

不但可以peek,还map可以跳过。这是为了优化。例如,当count()调用终端操作时,单个项目没有意义peek,因为map此类操作不会改变当前项目的数量/计数。

下面是两个例子:


1.不会跳过Map 和 peek,因为过滤器可以预先更改项目数量。

long count = Stream.of("a", "aa")
    .peek(s -> System.out.println("#1"))
    .filter(s -> s.length() < 2)
    .peek(s -> System.out.println("#2"))
    .map(s -> {
        System.out.println("#3");
        return s.length();
    })
    .count();
Run Code Online (Sandbox Code Playgroud)
#1
#2
#3
#1
1
Run Code Online (Sandbox Code Playgroud)

2.由于项目数量不变,因此跳过Map 和 peek 。

long count = Stream.of("a", "aa")
    .peek(s -> System.out.println("#1"))
  //.filter(s -> s.length() < 2)
    .peek(s -> System.out.println("#2"))
    .map(s -> {
        System.out.println("#3");
        return s.length();
    })
    .count();
Run Code Online (Sandbox Code Playgroud)
2
Run Code Online (Sandbox Code Playgroud)

重要提示:这些方法应该没有副作用(它们如上所述,但仅用于示例)。

一般来说,不鼓励流操作的行为参数产生副作用,因为它们通常会导致无意中违反无状态性要求,以及其他线程安全隐患。

下面的实现是危险的。假设callRestApi方法执行 REST 调用,则不会执行该调用,因为 Stream 违反了副作用。

long count = Stream.of("url1", "url2")
    .map(string -> callRestApi(HttpMethod.POST, string))
    .count();
Run Code Online (Sandbox Code Playgroud)
/**
 * Performs a REST call
 */
public String callRestApi(HttpMethod httpMethod, String url);
Run Code Online (Sandbox Code Playgroud)

  • 嗯,理论上,在示例 1 中,可以跳过最后一个“map”操作,因为在最后一个“filter”调用之后,元素计数无法更改。 (4认同)
  • @MCEmperor 有很多理论上的优化机会,目前尚未使用,但可能会在未来版本中使用。这就是依赖缺乏合法优化的做法如此危险的原因。 (2认同)
  • 如果你想获得更多乐趣,请使用 `IntStream.iterate(1, i -&gt; i + 1) .flatMap(i -&gt; IntStream.range(i, i + 10)) .peek(System.out:: println) .filter(i -&gt; i == 2) .findFirst() .ifPresent(System.out::println);` 并比较 Java 8 输出和 Java 11 输出。然后,您可以在某处插入`.parallel()`,然后看看会发生什么...... (2认同)

Ale*_*nko 13

peek()是一个中间操作,它期望消​​费者对流的元素执行操作(副作用)。

如果流管道不包含可以更改流中元素数量的中间操作takeWhile,例如、filterlimit等,并以终端操作 count()结束,并且流源允许评估流中的元素数量然后count()简单地询问并返回结果。所有中间操作都得到优化。

注意:这种操作优化自Java 9count()以来就存在(请参阅 API 注释),与没有直接关系,它会影响每个不改变流中元素数量的中间操作(目前这些是、、)。peek()map()sorted()peek()

还有更多内容

peek()在其他中间业务中具有非常特殊的利基。

从本质上讲,它peek()不同于其他中间操作,例如map()以及导致副作用的终端操作(就像这样做),为到达它们的每个元素执行最终操作,即和。peek()forEach()forEachOrdered()

关键是它对流执行的结果peek() 没有影响。它永远不会影响终端操作产生的结果,无论是值还是最终操作。

也就是说,如果我们peek()从管道中扔掉,并不会影响终端的运行

该方法的文档peek()以及Stream API 文档警告其操作可能会被省略,您不应该依赖它。

引用自以下文档peek()

如果流实现能够优化部分或全部元素的生成(例如使用 findFirst 等短路操作,或者在 count() 中描述的示例中),则不会为这些元素调用该操作。

引用 API 文档副作用段落:

副作用的消失也可能令人惊讶。除了终端操作forEach和之外forEachOrdered,当流实现可以优化行为参数的执行而不影响计算结果时,行为参数的副作用可能并不总是被执行。

这是流的示例(链接到源),其中除了以下内容之外,没有任何中间操作被省略peek()

Stream.of(1, 2, 3)
    .parallel()
    .peek(System.out::println)
    .skip(1)
    .map(n -> n * 10)
    .forEach(System.out::println);
Run Code Online (Sandbox Code Playgroud)

因此,在此管道中,peek()skip()可能希望它在控制台上显示源中的每个元素。但是,它不会发生(元素1不会被打印)。由于peek()其性质,可以在不破坏代码的情况下对其进行优化,即不影响终端操作。

这就是为什么文档明确指出此操作专门用于调试目的,并且不应为它分配在任何情况下都需要执行的操作。