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.