2

As far as I know, if I have a stream with two filter they will be combined with && in the bytecode.

for example

IntStream.range(1,10)
  .filter(i -> i % 2 == 0)
  .filter(i -> i % 3 == 0)
  .sum();

would be something like i % 2 == 0 && i % 3 == 0.

Does peek affect this?

If you were to peek after the first filer you get 2468 and if you peek after the second you get only 6 (of course).

But if you peek at both places

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();

you get 24668.

My assumption is that this must mean that the operation is somehow separated as a result of the peek call. Something like

if(i%2==0)
  peek
  if(i%3==0)

Is this true and if so does it affect performance (I assume it would not).

2 Answers 2

6

The Stream API is an ordinary Java API, as you can see yourself. It’s filter method receives an arbitrary Predicate instance, be it implemented via lambda expression or ordinary class (or enum to name all possibilities).

If you invoke filter two times subsequently, the underlying implementation could join them to a single filter by invoking Predicate.and but whether it does or not has no consequences in the case of predicates implemented via lambda expressions.

Unlike custom Predicate implementations, which could override the and method and provide something optimized in the case they recognize the second Predicate implementation, the classes generated for lambda expression do not override any default method, but just the one abstract functional method, here Predicate.test, so in this case, invoking and will get what the default method returns, a new Predicate which holds a reference to both source predicates and combines them, much like a Stream implementation which doesn’t uses Predicate.and will do.

So there are no substantial differences between these possible implementations and there is none if you insert another action like a Consumer passed to peek in-between. Of course, it now does more than without this action, so it has a performance impact, but not regarding the predicates.

But your general misconception seems to be that you think there was a major difference between:

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

and

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

Have a look at the byte code of the compiled methods:

//  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

As you can see, the insertion of a print statement causes, well, exactly an insertion of a print statement, nothing more, nothing less. Or, in other words, the && operator isn’t a magic fusion thing that differs from two nested if statements. Both do exactly the same, semantically and in byte code.

The same applies to the Stream API usage, though there, the code will be more complicated as the conditional expressions are expressed as Predicate instances and the inserted statements are Consumers. But in the best case, the HotSpot optimizer will generate exactly the same optimized native code for the Stream variant as for the loop variant.

Sign up to request clarification or add additional context in comments.

4 Comments

I thought lambdas are compiled to synthetic methods, not classes (referring to "the classes generated for lambda expression"). Which is correct?
Thank you for your detailed answer, that was exactly what I wanted to know. I expected the two to be of equal performance, I was just unsure if I my assumption was true. It would seem it was, but now I have a deeper understanding of why. Thank you!
@erickson: lambda expression are compiled into synthetic methods for which the JRE will generate a class which fulfills the functional interface and invokes that synthetic method. A lot of properties of the generated class is intentionally unspecified, but the fact that they don’t override default methods is fixed in JLS§15.27.4: “The class overrides no other methods of the targeted functional interface type or other interface types mentioned above, although it may override methods of the Object class
Okay, that makes sense. I realized there has to be an object to implement the interface, but I wasn't sure if there was some sort of reflective/dynamic proxy implementation, or actually a synthetic class at compile time.
0

It isn't optimized at all at the bytecode level. Each lambda is a seperate method. Java relies on the JVM to optimize everything transparently at runtime.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.