Sometimes ago I stumbled upon a bunch of articles mainly written in 2014 or 2015 which compare different loops speed in Java. While the articles are usually well documented, I found they are quite dated. So from the technology point of view, the results are obsolete. Many things have changed since then. For instance, CPUs have seen the massive upgrade and three versions of JDK have released.
So to have a more realistic view of Java lambda speed, I decided to do a similar experiment but this time with JDK 11. The aim obviously is to figure out whether the for-each
Parallel Stream
can outperform the good for-loop
iterator
, enhanced for-loop
I’m interested to know whether the lambda functions can utilize the modern CPU improvements efficiently to beat the good old loops. And that is because I’m passionate to use functional features of Java in my code heavily. But often I receive criticisms/objections quoting the articles that lambda functions perform worse.
For this experiment, I will run a benchmark on five different type of loops as follow:
- for-loop
- enhanced for-loop
- iterator
- for-each lambda
- parallel stream for-each lambda
To simplify the test I chose to ArrayList
String
I ran the experiment on ten iterations on a 2018 Macbook pro machine equipped with Intel(R) Core(TM) i7-8559U CPU @ 2.70GHz, and 16GB RAM with OpenJDK 11.0.1.
I have run the experiment using the code below (bear the code cleanness):
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import java.util.stream.IntStream;
public class LoopBenchmark {
private static final int THOUSAND = 1000;
private static final int TEN_THOUSAND = 10000;
private static final int HUNDRED_THOUSAND = 100000;
private static final int ONE_MILLION = 1000000;
public static void main(String[] args) {
List<String> thousandItems = new ArrayList<>();
List<String> tenThousandItems = new ArrayList<>();
List<String> hundredThousandItems = new ArrayList<>();
List<String> oneMillionItems = new ArrayList<>();
List<String> results = new ArrayList<>();
IntStream.range(0, THOUSAND).forEach(x -> thousandItems.add(UUID.randomUUID().toString()));
IntStream.range(0, TEN_THOUSAND).forEach(x -> tenThousandItems.add(UUID.randomUUID().toString()));
IntStream.range(0, HUNDRED_THOUSAND).forEach(x -> hundredThousandItems.add(UUID.randomUUID().toString()));
IntStream.range(0, ONE_MILLION).forEach(x -> oneMillionItems.add(UUID.randomUUID().toString()));
IntStream.range(0, 10).forEach(x -> {
// for-loop old school 1K
Instant start = Instant.now();
for (int i = 0; i < thousandItems.size(); i++) {
System.out.println(thousandItems.get(i));
}
Instant end = Instant.now();
Duration timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional for-loop 1K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// for-loop old school 10K
start = Instant.now();
for (int i = 0; i < tenThousandItems.size(); i++) {
System.out.println(tenThousandItems.get(i));
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional for-loop 10K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// for-loop old school 100K
start = Instant.now();
for (int i = 0; i < hundredThousandItems.size(); i++) {
System.out.println(hundredThousandItems.get(i));
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional for-loop 100K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// for-loop old school 1M
start = Instant.now();
for (int i = 0; i < oneMillionItems.size(); i++) {
System.out.println(oneMillionItems.get(i));
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional for-loop 1M: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// iterator 1K
Iterator<String> iterator = thousandItems.iterator();
start = Instant.now();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional iterator 1K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// iterator 10K
iterator = tenThousandItems.iterator();
start = Instant.now();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional iterator 10K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// iterator 100K
iterator = hundredThousandItems.iterator();
start = Instant.now();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional iterator 100K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// iterator 1M
iterator = oneMillionItems.iterator();
start = Instant.now();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("Traditional iterator 1M: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 1K for-loop object
start = Instant.now();
for (String item : thousandItems) {
System.out.println(item);
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-loop object 1K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 10K for-loop object
start = Instant.now();
for (String item : tenThousandItems) {
System.out.println(item);
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-loop object 10K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 100K for-loop object
start = Instant.now();
for (String item : hundredThousandItems) {
System.out.println(item);
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-loop object 100K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 1M for-loop object
start = Instant.now();
for (String item : oneMillionItems) {
System.out.println(item);
}
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-loop object 1M: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 1K for-each lambda
start = Instant.now();
thousandItems.forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-each lambda 1K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 10K for-each lambda
start = Instant.now();
tenThousandItems.forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-each lambda 10K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 100K for-each lambda
start = Instant.now();
hundredThousandItems.forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-each lambda 100K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 1M for-each lambda
start = Instant.now();
oneMillionItems.forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("for-each lambda 1M: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 1K parallel stream for-each lambda
start = Instant.now();
thousandItems.parallelStream().forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("parallel stream for-each lambda 1K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 10K parallel stream for-each lambda
start = Instant.now();
tenThousandItems.parallelStream().forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("parallel stream for-each lambda 10K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 100K parallel stream for-each lambda
start = Instant.now();
hundredThousandItems.parallelStream().forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("parallel stream for-each lambda 100K: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
// 1M parallel stream for-each lambda
start = Instant.now();
oneMillionItems.parallelStream().forEach(System.out::println);
end = Instant.now();
timeElapsed = Duration.between(start, end);
results.add(String.format("parallel stream for-each lambda 1M: %s milliseconds - experiment %s", timeElapsed.toMillis(), x));
});
Collections.sort(results);
results.forEach(System.out::println);
}
}
Results analysis
In general, each loop performs differently in different datasets. So coming with a single conclusion is quite tricky. What I can see clearly is parallel stream
performs worst in almost all experiment with exception to
Anyhow, in the following section, I discuss the benchmark result for each dataset.
The 1K dataset
As you can see in the below picture, enhanced for-loop
outperforms all other loops. Surprisingly, lambda for-each
performs worst by a large margin, which followed parallel-stream

One thing to note is even 5 milliseconds difference does not make anything significantly slower. I believe the 1K benchmark is not good enough to draw any conclusion from it.
The 10K dataset
By contrast to the previous dataset, for-each
lambda had a very good performance this time. It outperforms the for-loop
enhanced for-loop
parallel-stream

The 100K dataset
This time the winner is enhanced for-loop
for-each
parallel-stream

The 1M dataset
iterator
has the upper hand which followed enhanced for-loop
parallel-stream

Verdict
Even though lambda/functional loops didn’t perform better in any case than conventional loops, it does not imply that we should avoid using them. This is especially true about for-each
. The performance difference is so negligible (at least in JDK 11). In my honest opinion the features provided by for-each
like filter
, map
, … along the way outweighs this performance tradeoff. Not to mention the code clarity and readability.
Hence, I rather stick to my habit of using Java functional features.
Of parallel stream
parallel stream
parallel stream
fork-join
References
- https://aliteralmind.wordpress.com/2014/03/22/for_foreach/comment-page-1/
- https://www.programcreek.com/2014/01/enhanced-for-loop-vs-foreach-in-java-8/
- https://blog.jooq.org/2015/12/08/3-reasons-why-you-shouldnt-replace-your-for-loops-by-stream-foreach/
- https://jaxenter.com/java-performance-tutorial-how-fast-are-the-java-8-streams-118830.html
Inline/featured images credits
- Featured image by our-team on Freepik