Interesting that the interview question asks about the advantages, without asking about disadvantages, for there are are both.

Streams are a more declarative style. Or a more expressive style. It may be considered better to declare your intent in code, than to describe how it's done:

 return people
     .filter( p -> p.age() < 19)
     .collect(toList());

... says quite clearly that you're filtering matching elements from a list, whereas:

 List<Person> filtered = new ArrayList<>();
 for(Person p : people) {
     if(p.age() < 19) {
         filtered.add(p);
     }
 }
 return filtered;

Says "I'm doing a loop". The purpose of the loop is buried deeper in the logic.

Streams are often terser. The same example shows this. Terser isn't always better, but if you can be terse and expressive at the same time, so much the better.

Streams have a strong affinity with functions. Java 8 introduces lambdas and functional interfaces, which opens a whole toybox of powerful techniques. Streams provide the most convenient and natural way to apply functions to sequences of objects.

Streams encourage less mutability. This is sort of related to the functional programming aspect -- the kind of programs you write using streams tend to be the kind of programs where you don't modify objects.

Streams encourage looser coupling. Your stream-handling code doesn't need to know the source of the stream, or its eventual terminating method.

Streams can succinctly express quite sophisticated behaviour. For example:

 stream.filter(myfilter).findFirst();

Might look at first glance as if it filters the whole stream, then returns the first element. But in fact findFirst() drives the whole operation, so it efficiently stops after finding one item.

Streams provide scope for future efficiency gains. Some people have benchmarked and found that single-threaded streams from in-memory Lists or arrays can be slower than the equivalent loop. This is plausible because there are more objects and overheads in play.

But streams scale. As well as Java's built-in support for parallel stream operations, there are a few libraries for distributed map-reduce using Streams as the API, because the model fits.

Disadvantages?

Performance: A for loop through an array is extremely lightweight both in terms of heap and CPU usage. If raw speed and memory thriftiness is a priority, using a stream is worse.

Familiarity.The world is full of experienced procedural programmers, from many language backgrounds, for whom loops are familiar and streams are novel. In some environments, you want to write code that's familiar to that kind of person.

Cognitive overhead. Because of its declarative nature, and increased abstraction from what's happening underneath, you may need to build a new mental model of how code relates to execution. Actually you only need to do this when things go wrong, or if you need to deeply analyse performance or subtle bugs. When it "just works", it just works.

Debuggers are improving, but even now, when you're stepping through stream code in a debugger, it can be harder work than the equivalent loop, because a simple loop is very close to the variables and code locations that a traditional debugger works with.

Answer from slim on Stack Overflow
Top answer
1 of 5
421

Interesting that the interview question asks about the advantages, without asking about disadvantages, for there are are both.

Streams are a more declarative style. Or a more expressive style. It may be considered better to declare your intent in code, than to describe how it's done:

 return people
     .filter( p -> p.age() < 19)
     .collect(toList());

... says quite clearly that you're filtering matching elements from a list, whereas:

 List<Person> filtered = new ArrayList<>();
 for(Person p : people) {
     if(p.age() < 19) {
         filtered.add(p);
     }
 }
 return filtered;

Says "I'm doing a loop". The purpose of the loop is buried deeper in the logic.

Streams are often terser. The same example shows this. Terser isn't always better, but if you can be terse and expressive at the same time, so much the better.

Streams have a strong affinity with functions. Java 8 introduces lambdas and functional interfaces, which opens a whole toybox of powerful techniques. Streams provide the most convenient and natural way to apply functions to sequences of objects.

Streams encourage less mutability. This is sort of related to the functional programming aspect -- the kind of programs you write using streams tend to be the kind of programs where you don't modify objects.

Streams encourage looser coupling. Your stream-handling code doesn't need to know the source of the stream, or its eventual terminating method.

Streams can succinctly express quite sophisticated behaviour. For example:

 stream.filter(myfilter).findFirst();

Might look at first glance as if it filters the whole stream, then returns the first element. But in fact findFirst() drives the whole operation, so it efficiently stops after finding one item.

Streams provide scope for future efficiency gains. Some people have benchmarked and found that single-threaded streams from in-memory Lists or arrays can be slower than the equivalent loop. This is plausible because there are more objects and overheads in play.

But streams scale. As well as Java's built-in support for parallel stream operations, there are a few libraries for distributed map-reduce using Streams as the API, because the model fits.

Disadvantages?

Performance: A for loop through an array is extremely lightweight both in terms of heap and CPU usage. If raw speed and memory thriftiness is a priority, using a stream is worse.

Familiarity.The world is full of experienced procedural programmers, from many language backgrounds, for whom loops are familiar and streams are novel. In some environments, you want to write code that's familiar to that kind of person.

Cognitive overhead. Because of its declarative nature, and increased abstraction from what's happening underneath, you may need to build a new mental model of how code relates to execution. Actually you only need to do this when things go wrong, or if you need to deeply analyse performance or subtle bugs. When it "just works", it just works.

Debuggers are improving, but even now, when you're stepping through stream code in a debugger, it can be harder work than the equivalent loop, because a simple loop is very close to the variables and code locations that a traditional debugger works with.

2 of 5
36

Syntactic fun aside, Streams are designed to work with potentially infinitely large data sets, whereas arrays, Collections, and nearly every Java SE class which implements Iterable are entirely in memory.

A disadvantage of a Stream is that filters, mappings, etc., cannot throw checked exceptions. This makes a Stream a poor choice for, say, intermediate I/O operations.

🌐
Baeldung
baeldung.com › home › java › java collections › the difference between collection.stream().foreach() and collection.foreach()
The Difference Between stream().forEach() and forEach() | Baeldung
September 17, 2025 - If we want to use functional-style Java, we can also use forEach(). ... In this simple case, it doesn’t make a difference which forEach() we use. Collection.forEach() uses the collection’s iterator (if one is specified), so the processing order of the items is defined. In contrast, the processing order of Collection.stream().forEach() is undefined.
Discussions

Why is the Java Stream.forEach method faster than other loops in certain situations? - Stack Overflow
I'm currently taking on a project where I'm measuring the speed of different types of loops in Java using the Java Microbenchmark Harness (JMH) framework. I got some interesting results regarding s... More on stackoverflow.com
🌐 stackoverflow.com
java - What is difference between Collection.stream().forEach() and Collection.forEach()? - Stack Overflow
I understand that with .stream(), I can use chain operations like .filter() or use parallel stream. But what is difference between them if I need to execute small operations (for example, printing ... More on stackoverflow.com
🌐 stackoverflow.com
java - Is Collection.stream().filter().forEach() inefficient compared to a standard for each loop? - Software Engineering Stack Exchange
But actually the iterations are interleaved with each other, and both may terminate early if the result is already known - that's the beauty of streams. It's definitely worth spending 15 minutes to read up on the streams in Java 8; as Venkat Subramaniam writes, ''Lambda expressions are the ... More on softwareengineering.stackexchange.com
🌐 softwareengineering.stackexchange.com
.forEach() on a list vs .forEach() on a stream. What's the difference?
Please ensure that: Your code is properly formatted as code block - see the sidebar (About on mobile) for instructions You include any and all error messages in full - best also formatted as code block You ask clear questions You demonstrate effort in solving your question/problem - plain posting your assignments is forbidden (and such posts will be removed) as is asking for or giving solutions. If any of the above points is not met, your post can and will be removed without further warning. Code is to be formatted as code block (old reddit/markdown editor: empty line before the code, each code line indented by 4 spaces, new reddit: https://i.imgur.com/EJ7tqek.png ) or linked via an external code hoster, like pastebin.com, github gist, github, bitbucket, gitlab, etc. Please, do not use triple backticks (```) as they will only render properly on new reddit, not on old reddit. Code blocks look like this: public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World!"); } } You do not need to repost unless your post has been removed by a moderator. Just use the edit function of reddit to make sure your post complies with the above. If your post has remained in violation of these rules for a prolonged period of time (at least an hour), a moderator may remove it at their discretion. In this case, they will comment with an explanation on why it has been removed, and you will be required to resubmit the entire post following the proper procedures. To potential helpers Please, do not help if any of the above points are not met, rather report the post. We are trying to improve the quality of posts here. In helping people who can't be bothered to comply with the above points, you are doing the community a disservice. I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns. More on reddit.com
🌐 r/learnjava
9
5
September 29, 2023
Top answer
1 of 3
3

Alternative 2 looks best to me in this case

1. Readability

  • Alternative 2 has less number of lines
  • Alternative 2 read more close to return a list containing number divisible by 6 from numList, while forEach approach means add number divisible by 6 from numList to secondInts
  • filter(i -> i % 6 == 0) is straight forward and
    if(i % 6 != 0) {
        return;
    }
    
    require some time for human brain to process.

2. Performance

From Stream.toList()

Implementation Note: Most instances of Stream will override this method and provide an implementation that is highly optimized compared to the implementation in this interface.

We benefit from optimization from JDK by using Stream API.

And in this case, using forEach and adding element one by one will be slower, especially when the list is large. It is because ArrayList will need to extend it capacity whenever the list full, while Stream implementation ImmutableCollections.listFromTrustedArrayNullsAllowed just store the result array into ListN.

One more point to note about parallelism:
From Stream#forEach

The behavior of this operation is explicitly nondeterministic. For parallel stream pipelines, this operation does not guarantee to respect the encounter order of the stream, as doing so would sacrifice the benefit of parallelism. For any given element, the action may be performed at whatever time and in whatever thread the library chooses. If the action accesses shared state, it is responsible for providing the required synchronization.

numbersList.stream().parallel()
       .forEach(i -> {
           if(i % 6 != 0) {
               return;
           }
           secondInts.add(i);
       });

Will provide unexpected result, while

List<Integer> third = numbersList.stream().parallel()
      .filter(i -> i % 6 == 0).sorted().forEach()
      .toList();

is totally fine.

3. Flexibility

Imagine you want the filtered list to be sorted, in forEach approach, you can do it like:

numbersList.stream().sorted().
       .forEach(i -> {
           if(i % 6 != 0) {
               return;
           }
           secondInts.add(i);
       });

Which is much slower compared to

numbersList.stream()
        .filter(i -> i % 6 == 0)
        .sorted()
        .toList();

As we need to sort the whole numbersList instead of filtered.

Or if you want to limit your result to 10 elements, it is not straight forward to do so with forEach, but just as simple as adding limit(10) when using stream.

4. Less error prone

Stream API usually return Immutable object by default.

From Stream.toList()

Implementation Requirements: The implementation in this interface returns a List produced as if by the following: Collections.unmodifiableList(new ArrayList<>(Arrays.asList(this.toArray())))

Meaning that the returned list is immutable by default. Some advantages of immutability are:

  1. You can safely pass the list around to different method without worrying the list is modified.
  2. Immutable list are thread safe.

Read Pros. / Cons. of Immutability vs. Mutability for further discussion.

2 of 3
1

I don’t think it’s an issue of advantages. Each mechanism has a specific purpose.

.forEach() returns void so it doesn’t have an output. The intent is that the elements that the forEach iterates through are not modified. The data in the elements are used for some sort of calculation. I find that forEach is used much less than map. It’s a terminal point in a pipeline.

.filter() takes a stream as input and emits a filtered stream as output. It is for filtering.

.map() is like forEach but it emits a stream of modified objects. It allows the same modification to be done on each each element so that it can be saved, filtered or manipulated further.

.toList is a handy shortcut to turn a stream into a list. Using forEach(List::add) where a toList() will do the work is a terrible idea. You’re preventing Java from bulking the activity.

🌐
Medium
medium.com › @kouomeukevin › why-you-should-use-java-for-loop-instead-of-stream-foreach-method-da8227990fad
Why You Should use Java for Loop Instead of Stream.forEach Method | by Kevin Kouomeu | Medium
April 16, 2024 - But if you are interested in performance optimization, just use classic for loops. ... The purpose of Streams is not as some sort of replacement to for loops. If you just have a list you need to iterate over, it’s obvious that Stream.forEach isn't necessary.
Top answer
1 of 1
10

This answer talks about java.util.ArrayList. Other Collection implementations might (and do!) show completely different results.

In short: because streams use Spliterator instead of Iterator

Explanation:

Iterator based iteration is based on hasNext and next method calls, the second of which mutates the Iterator instance. This brings some performance costs with it. Spliterator supports bulk iterating. Read more about this here. The enhanced for loops (for (T e : iterable) { ... }) seem to use an Iterator.

Performance should usually be a secondary concern; you should use the constructs that best describe your intentions. While due to backward compatibility reasons it's going to be hard, maybe the performance difference between spliterator based forEach and enhanced for loops (on ArrayList) will disappear in the future.

What about indexed for loops? (List#get(int))
They show worse-than-spliterator performance partially because they need to verify the index. The other reasons probably include method calls eg. to get the data at an index, whereas Spliterator accesses the array directly. But this is pure speculation.

Tiny JMH Benchmark

Below you can see a tiny benchmark that confirms the stated theories. Please note that optimally the benchmark should have been ran for longer.

@State(Scope.Benchmark)
@Fork(value = 2)
@Warmup(iterations = 2, time = 3)
@Measurement(iterations = 2, time = 3)
public class A {
    
    public List<Object> list;
    
    @Setup
    public void setup() {
        list = new ArrayList<>();
        for (int i = 0; i < 1000; i++) list.add(i);
    }
    
    @Benchmark
    public void collectionForEach(Blackhole hole) {
        list.forEach(hole::consume);
    }
    
    @Benchmark
    public void iteratorFor(Blackhole hole) {
        for (Iterator<Object> iterator = list.iterator(); iterator.hasNext(); ) {
            hole.consume(iterator.next());
        }
    }
    
    @Benchmark
    public void enhancedFor(Blackhole hole) {
        for (Object e : list) {
            hole.consume(e);
        }
    }
    
    @Benchmark
    public void iteratorForEach(Blackhole hole) {
        list.iterator().forEachRemaining(hole::consume);
    }
    
    @Benchmark
    public void indexedFor(Blackhole hole) {
        for (int i = 0, size = list.size(); i < size; i++) {
            hole.consume(list.get(i));
        }
    }
    
    @Benchmark
    public void streamForEach(Blackhole hole) {
        list.stream().forEach(hole::consume);
    }
    
    @Benchmark
    public void spliteratorForEach(Blackhole hole) {
        list.spliterator().forEachRemaining(hole::consume);
    }
}

And the results. "ops/s" means operations/second, higher is better.

Benchmark              Mode  Cnt       Score      Error  Units
A.collectionForEach   thrpt    4  158047.064 ±  959.534  ops/s
A.iteratorFor         thrpt    4  177338.462 ± 3245.129  ops/s
A.enhancedFor         thrpt    4  177508.037 ± 1629.494  ops/s
A.iteratorForEach     thrpt    4  184919.605 ± 1922.114  ops/s
A.indexedFor          thrpt    4  193318.529 ± 2715.611  ops/s
A.streamForEach       thrpt    4  280963.272 ± 2253.621  ops/s
A.spliteratorForEach  thrpt    4  283264.539 ± 3055.967  ops/s
🌐
jOOQ
blog.jooq.org › 3-reasons-why-you-shouldnt-replace-your-for-loops-by-stream-foreach
3 Reasons why You Shouldn’t Replace Your for-loops by Stream.forEach() – Java, SQL and jOOQ.
April 2, 2020 - Performance is killed when you chose java, so just put the work to adapt to the new world, even if you’re already old. ... The author is just saying to use you common sense before running and changing all “for” loops to streams. By the way not necessarily new == better, old == worse… Once you get older and wiser you will realize that ... While list.forEach() produces very little overhead, chances are that you will soon add the stream() call nonetheless, because you’re going to get bored of putting an if statement in that lambda (replacing it by filter) or some local variable assignments (replacing them by map), or a nested forEach() call (replacing them by flatMap()).
🌐
Java Code Geeks
javacodegeeks.com › home › core java
For Loops vs. Stream.forEach: When to Use Which - Java Code Geeks
August 11, 2024 - In summary, while for loops are ideal for simpler tasks and scenarios where precise control and performance are critical, Stream.forEach excels in complex data processing, enhancing readability, and enabling easy parallelism.
Find elsewhere
Top answer
1 of 5
341

For simple cases such as the one illustrated, they are mostly the same. However, there are a number of subtle differences that might be significant.

One issue is with ordering. With Stream.forEach, the order is undefined. It's unlikely to occur with sequential streams, still, it's within the specification for Stream.forEach to execute in some arbitrary order. This does occur frequently in parallel streams. By contrast, Iterable.forEach is always executed in the iteration order of the Iterable, if one is specified.

Another issue is with side effects. The action specified in Stream.forEach is required to be non-interfering. (See the java.util.stream package doc.) Iterable.forEach potentially has fewer restrictions. For the collections in java.util, Iterable.forEach will generally use that collection's Iterator, most of which are designed to be fail-fast and which will throw ConcurrentModificationException if the collection is structurally modified during the iteration. However, modifications that aren't structural are allowed during iteration. For example, the ArrayList class documentation says "merely setting the value of an element is not a structural modification." Thus, the action for ArrayList.forEach is allowed to set values in the underlying ArrayList without problems.

The concurrent collections are yet again different. Instead of fail-fast, they are designed to be weakly consistent. The full definition is at that link. Briefly, though, consider ConcurrentLinkedDeque. The action passed to its forEach method is allowed to modify the underlying deque, even structurally, and ConcurrentModificationException is never thrown. However, the modification that occurs might or might not be visible in this iteration. (Hence the "weak" consistency.)

Still another difference is visible if Iterable.forEach is iterating over a synchronized collection. On such a collection, Iterable.forEach takes the collection's lock once and holds it across all the calls to the action method. The Stream.forEach call uses the collection's spliterator, which does not lock, and which relies on the prevailing rule of non-interference. The collection backing the stream could be modified during iteration, and if it is, a ConcurrentModificationException or inconsistent behavior could result.

2 of 5
64

This answer concerns itself with the performance of the various implementations of the loops. Its only marginally relevant for loops that are called VERY OFTEN (like millions of calls). In most cases the content of the loop will be by far the most expensive element. For situations where you loop really often, this might still be of interest.

You should repeat this tests under the target system as this is implementation specific, (full source code).

I run openjdk version 1.8.0_111 on a fast Linux machine.

I wrote a test that loops 10^6 times over a List using this code with varying sizes for integers (10^0 -> 10^5 entries).

The results are below, the fastest method varies depending on the amount of entries in the list.

But still under worst situations, looping over 10^5 entries 10^6 times took 100 seconds for the worst performer, so other considerations are more important in virtually all situations.

public int outside = 0;

private void iteratorForEach(List<Integer> integers) {
    integers.forEach((ii) -> {
        outside = ii*ii;
    });
}

private void forEach(List<Integer> integers) {
    for(Integer next : integers) {
        outside = next * next;
    }
}

private void forCounter(List<Integer> integers) {
    for(int ii = 0; ii < integers.size(); ii++) {
        Integer next = integers.get(ii);
        outside = next*next;
    }
}

private void iteratorStream(List<Integer> integers) {
    integers.stream().forEach((ii) -> {
        outside = ii*ii;
    });
}

Here are my timings: milliseconds / function / number of entries in list. Each run is 10^6 loops.

                           1    10    100    1000    10000
       iterator.forEach   27   116    959    8832    88958
               for:each   53   171   1262   11164   111005
         for with index   39   112    920    8577    89212
iterable.stream.forEach  255   324   1030    8519    88419

If you repeat the experiment, I posted the full source code. Please do edit this answer and add you results with a notation of the tested system.


Using a MacBook Pro, 2.5 GHz Intel Core i7, 16 GB, macOS 10.12.6:

                           1    10    100    1000    10000
       iterator.forEach   27   106   1047    8516    88044
               for:each   46   143   1182   10548   101925
         for with index   49   145    887    7614    81130
iterable.stream.forEach  393   397   1108    8908    88361

Java 8 Hotspot VM - 3.4GHz Intel Xeon, 8 GB, Windows 10 Pro

                            1    10    100    1000    10000
        iterator.forEach   30   115    928    8384    85911
                for:each   40   125   1166   10804   108006
          for with index   30   120    956    8247    81116
 iterable.stream.forEach  260   237   1020    8401    84883

Java 11 Hotspot VM - 3.4GHz Intel Xeon, 8 GB, Windows 10 Pro
(same machine as above, different JDK version)

                            1    10    100    1000    10000
        iterator.forEach   20   104    940    8350    88918
                for:each   50   140    991    8497    89873
          for with index   37   140    945    8646    90402
 iterable.stream.forEach  200   270   1054    8558    87449

Java 11 OpenJ9 VM - 3.4GHz Intel Xeon, 8 GB, Windows 10 Pro
(same machine and JDK version as above, different VM)

                            1    10    100    1000    10000
        iterator.forEach  211   475   3499   33631   336108
                for:each  200   375   2793   27249   272590
          for with index  384   467   2718   26036   261408
 iterable.stream.forEach  515   714   3096   26320   262786

Java 8 Hotspot VM - 2.8GHz AMD, 64 GB, Windows Server 2016

                            1    10    100    1000    10000
        iterator.forEach   95   192   2076   19269   198519
                for:each  157   224   2492   25466   248494
          for with index  140   368   2084   22294   207092
 iterable.stream.forEach  946   687   2206   21697   238457

Java 11 Hotspot VM - 2.8GHz AMD, 64 GB, Windows Server 2016
(same machine as above, different JDK version)

                            1    10    100    1000    10000
        iterator.forEach   72   269   1972   23157   229445
                for:each  192   376   2114   24389   233544
          for with index  165   424   2123   20853   220356
 iterable.stream.forEach  921   660   2194   23840   204817

Java 11 OpenJ9 VM - 2.8GHz AMD, 64 GB, Windows Server 2016
(same machine and JDK version as above, different VM)

                            1    10    100    1000    10000
        iterator.forEach  592   914   7232   59062   529497
                for:each  477  1576  14706  129724  1190001
          for with index  893   838   7265   74045   842927
 iterable.stream.forEach 1359  1782  11869  104427   958584

The VM implementation you choose also makes a difference Hotspot/OpenJ9/etc.

🌐
Baeldung
baeldung.com › home › java › java streams › streams vs. loops in java
Streams vs. Loops in Java | Baeldung
January 8, 2024 - We can see that in our example, the for-loops performed much better than the streams from the performance perspective.
🌐
GeeksforGeeks
geeksforgeeks.org › java › comparing-streams-to-loops-in-java
Comparing Streams to Loops in Java - GeeksforGeeks
October 20, 2022 - Debuggers are improving, but even ... traditional debugger works with. ... If you have a small list; for loops perform better, if you have a huge list; a parallel stream will perform better....
🌐
LinkedIn
linkedin.com › pulse › exploring-performance-differences-java-8-stream-vs-normal-mustafa-pj5mc
Exploring Performance Differences: Java 8 Stream vs Normal For-Each Loop
April 19, 2024 - Streams offer a more expressive and functional approach to work with collections, which frequently results in code that is easier to read and maintain. Traditional for-each loops, on the other hand, are more straightforward and effective, ...
🌐
JDriven Blog
blog.jdriven.com › 2019 › 10 › loop
Java streams vs for loop - JDriven Blog
November 14, 2019 - And since parallel streams have quite a bit of overhead, it is not advised to use these unless you are sure it is worth the overhead. So although the difference is not that big, for loops win by pure performance. That being said; performance is not the only important metric to measure your code by.
🌐
Blogger
minborgsjavapot.blogspot.com › 2019 › 09 › java-performance-for-eaching-vs.html
Minborg's Java Pot: Java Performance: For-looping vs. Streaming
Benchmark Mode Cnt Score Error Units ForBenchmark.forDown thrpt 5 311419.166 ± 4201.724 ops/s ForBenchmark.forUp thrpt 5 309598.916 ± 12998.579 ops/s ForBenchmark.stream thrpt 5 312360.089 ± 8291.792 ops/s It might come as a surprise for some that the stream solution is the fastest one, albeit by a margin that is well within error margins. In a previous article, I presented some code metric advantages with streams and Declarative programming compared to traditional Imperative code. I have not tested performance for cold code sections (i.e.
🌐
Medium
medium.com › @KosteRico › for-each-loop-vs-foreach-method-in-java-11-which-one-is-faster-8a5120c0c8c3
For-each loop vs “forEach” method in Java 11. Which one is faster? | by Konstantin Parakhin | Medium
September 28, 2021 - Every good software developer strive for better code performance. Recently I read an article 3 Reasons why You Shouldn’t Replace Your for-loops by Stream.forEach() in Java and found 1st one quite interesting.
🌐
DZone
dzone.com › coding › java › java performance: for-looping vs. streaming
Java Performance: For-Looping vs. Streaming
September 9, 2019 - With Speedment HyperStream, it is possible to get similar performance with data from databases. Read more here on HyperStream. On some commonly used hardware/JVMs, it does not matter if we iterate upwards or downwards in our for-loops. More modern JVMs are able to optimize stream iterations so they have equivalent or even better performance than for-loops.
🌐
Medium
medium.com › @ezzeddinebilel › java-stream-and-for-loop-a-balanced-view-on-performance-a38e45f271e5
Java Stream and For Loop: A Balanced View on Performance | by Bilel Ezzeddine | Medium
February 18, 2024 - However, as we increase the text size, the performance gap between the for loop and stream approaches narrows significantly. Eventually, for larger text sizes, the performance on both approaches becomes comparable, with neither significantly ...
Top answer
1 of 6
225
  1. Stop using LinkedList for anything but heavy removing from the middle of the list using iterator.

  2. Stop writing benchmarking code by hand, use JMH.

Proper benchmarks:

@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
@OperationsPerInvocation(StreamVsVanilla.N)
public class StreamVsVanilla {
    public static final int N = 10000;

    static List<Integer> sourceList = new ArrayList<>();
    static {
        for (int i = 0; i < N; i++) {
            sourceList.add(i);
        }
    }

    @Benchmark
    public List<Double> vanilla() {
        List<Double> result = new ArrayList<>(sourceList.size() / 2 + 1);
        for (Integer i : sourceList) {
            if (i % 2 == 0){
                result.add(Math.sqrt(i));
            }
        }
        return result;
    }

    @Benchmark
    public List<Double> stream() {
        return sourceList.stream()
                .filter(i -> i % 2 == 0)
                .map(Math::sqrt)
                .collect(Collectors.toCollection(
                    () -> new ArrayList<>(sourceList.size() / 2 + 1)));
    }
}

Result:

Benchmark                   Mode   Samples         Mean   Mean error    Units
StreamVsVanilla.stream      avgt        10       17.588        0.230    ns/op
StreamVsVanilla.vanilla     avgt        10       10.796        0.063    ns/op

Just as I expected stream implementation is fairly slower. JIT is able to inline all lambda stuff but doesn't produce as perfectly concise code as vanilla version.

Generally, Java 8 streams are not magic. They couldn't speedup already well-implemented things (with, probably, plain iterations or Java 5's for-each statements replaced with Iterable.forEach() and Collection.removeIf() calls). Streams are more about coding convenience and safety. Convenience -- speed tradeoff is working here.

2 of 6
20

1) You see time less than 1 second using you benchmark. That means there can be strong influence of side effects on your results. So, I increased your task 10 times

    int max = 10_000_000;

and ran your benchmark. My results:

Collections: Elapsed time:   8592999350 ns  (8.592999 seconds)
Streams: Elapsed time:       2068208058 ns  (2.068208 seconds)
Parallel streams: Elapsed time:  7186967071 ns  (7.186967 seconds)

without edit (int max = 1_000_000) results were

Collections: Elapsed time:   113373057 ns   (0.113373 seconds)
Streams: Elapsed time:       135570440 ns   (0.135570 seconds)
Parallel streams: Elapsed time:  104091980 ns   (0.104092 seconds)

It's like your results: stream is slower than collection. Conclusion: much time were spent for stream initialization/values transmitting.

2) After increasing task stream became faster (that's OK), but parallel stream remained too slow. What's wrong? Note: you have collect(Collectors.toList()) in you command. Collecting to single collection essentially introduces performance bottleneck and overhead in case of concurrent execution. It is possible to estimate the relative cost of overhead by replacing

collecting to collection -> counting the element count

For streams it can be done by collect(Collectors.counting()). I got results:

Collections: Elapsed time:   41856183 ns    (0.041856 seconds)
Streams: Elapsed time:       546590322 ns   (0.546590 seconds)
Parallel streams: Elapsed time:  1540051478 ns  (1.540051 seconds)

That' s for a big task! (int max = 10000000) Conclusion: collecting items to collection took majority of time. The slowest part is adding to list. BTW, simple ArrayList is used for Collectors.toList().

🌐
TutorialsPoint
tutorialspoint.com › difference-between-collection-stream-foreach-and-collection-foreach-in-java
Difference between Collection.stream().forEach() and Collection.forEach() in Java
Collection.stream().forEach() is also used for iterating the collection but it first convert the collection to the stream and then iterate over the stream of the collection therefore the processing order is undefined. It also throws the concurrent modification exception, if any structural changes ...