java stream.peek()如何影响字节码?

Chi*_*pen 2 java performance bytecode java-8 java-stream

据我所知,如果我有一个带有两个过滤器的流,它们将与字节码中的&&结合使用.

例如

IntStream.range(1,10)
  .filter(i -> i % 2 == 0)
  .filter(i -> i % 3 == 0)
  .sum();
Run Code Online (Sandbox Code Playgroud)

会像i%2 == 0 && i%3 == 0.

偷看会影响这个吗?

如果你在第一个文件管理器后偷看你得到2468,如果你偷看第二个,你只得到6(当然).

但如果你偷看这两个地方

IntStream.range(1,10)
            .filter(integer -> integer % 2 == 0)
            .peek(i-> System.out.print(i))
            .filter(integer -> integer % 3 == 0)
            .peek(i-> System.out.print(i))
            .sum();
Run Code Online (Sandbox Code Playgroud)

你得到24668.

我的假设是,这必然意味着操作在某种程度上是由于窥视调用而分开的.就像是

if(i%2==0)
  peek
  if(i%3==0)
Run Code Online (Sandbox Code Playgroud)

这是真的,如果是这样会影响性能(我认为不会).

Hol*_*ger 6

StreamAPI是一个普通的Java API,你可以看到自己.它的filter方法接收一个任意的Predicate实例,无论是通过lambda表达式还是普通的class(或enum命名所有可能性).

如果您filter随后调用了两次,则底层实现可以通过调用将它们连接到单个过滤器,Predicate.and但是在通过lambda表达式实现的谓词的情况下它是否有效.

与自定义Predicate实现不同,自定义实现可以覆盖and方法并在它们识别第二个Predicate实现的情况下提供优化的东西,为lambda表达式生成的类不会覆盖任何default方法,而只是一个abstract函数方法,这里Predicate.test,所以在这种情况下,调用and将得到该default方法返回的内容,一个新的Predicate,它包含对两个源谓词的引用并将它们组合起来,就像一个不使用的Stream实现Predicate.and一样.

因此,这些可能的实现之间没有实质性的差异,如果你插入另一个像Consumer传递给peek中间的动作则没有.当然,它现在比没有这个动作比较多,所以它一个性能的影响,但不是关于谓词.

但是你的一般误解似乎是你认为以下两者之间存在重大差异:

for(int i=1; i<10; i++) {
    if(i%2==0 && i%3==0)
        System.out.print(i);
}
Run Code Online (Sandbox Code Playgroud)

for(int i=1; i<10; i++) {
    if(i%2==0) {
        System.out.print(i);
        if(i%3==0)
            System.out.print(i);
    }
}
Run Code Online (Sandbox Code Playgroud)

看看编译方法的字节码:

//  first variant            second variant
  0: iconst_1              0: iconst_1
  1: istore_1              1: istore_1
  2: iload_1               2: iload_1
  3: bipush        10      3: bipush        10
  5: if_icmpge     33      5: if_icmpge     40
  8: iload_1               8: iload_1
  9: iconst_2              9: iconst_2
 10: irem                 10: irem
 11: ifne          27     11: ifne          34
                          14: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
                          17: iload_1
                          18: invokevirtual #3    // Method java/io/PrintStream.print:(I)V
 14: iload_1              21: iload_1
 15: iconst_3             22: iconst_3
 16: irem                 23: irem
 17: ifne          27     24: ifne          34
 20: getstatic     #2     27: getstatic     #2    // Field java/lang/System.out:Ljava/io/PrintStream;
 23: iload_1              30: iload_1
 24: invokevirtual #3     31: invokevirtual #3    // Method java/io/PrintStream.print:(I)V
 27: iinc          1, 1   34: iinc          1, 1
 30: goto          2      37: goto          2
 33: return               40: return
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,插入print语句恰好会导致插入print语句,仅此而已.或者,换句话说,&&运算符不是与两个嵌套if语句不同的神奇融合.两者在语义和字节代码中完全相同.

这同样适用于Stream API用法,尽管如此,代码将更复杂,因为条件表达式表示为Predicate实例,插入的语句是Consumers.但在最好的情况下,HotSpot优化器将为Stream变体生成与循环变体完全相同的优化本机代码.

  • @erickson:lambda表达式被*编译成合成方法,JRE将生成*一个实现功能接口并调用该合成方法的类.生成的类的许多属性是故意未指定的,但它们不覆盖`default`方法的事实[在JLS§15.27.4中修复](https://docs.oracle.com/javase/specs/ jls/se8/html/jls-15.html#jls-15.27.4-300-C):"*该类不会覆盖目标功能接口类型的其他方法或上面提到的其他接口类型,尽管它可能会覆盖方法`Object`类*" (2认同)