3 Reasons why You Shouldn’t Replace Your for-loops by Stream.forEach()

Awesome! We’re migrating our code base to Java 8. We’ll replace everything by functions. Throw out design patterns. Remove object orientation. Right! Let’s go!

Wait a minute

Java 8 has been out for over a year now, and the thrill has gone back to day-to-day business.

A non-representative study executed by baeldung.com from May 2015 finds that 38% of their readers have adopted Java 8. Prior to that, a late 2014 study by Typsafe had claimed 27% Java 8 adoption among their users.

What does it mean for your code-base?

Some Java 7 -> Java 8 migration refactorings are no-brainers. For instance, when passing a Callable to an ExecutorService:

ExecutorService s = ...

// Java 7 - meh...
Future<String> f = s.submit(
    new Callable<String>() {
        public String call() {
            return "Hello World";

// Java 8 - of course!
Future<String> f = s.submit(() -> "Hello World");

The anonymous class style really doesn’t add any value here.

Apart from these no-brainers, there are other, less obvious topics. E.g. whether to use an external vs. an internal iterator. See also this interesting read from 2007 by Neil Gafter on the timeless topic:

The result of the following two pieces of logic is the same

List<Integer> list = Arrays.asList(1, 2, 3);

// Old school
for (Integer i : list)

// "Modern"

I claim that the “modern” approach should be used with extreme care, i.e. only if you truly benefit from the internal, functional iteration (e.g. when chaining a set of operations via Stream’s map(), flatMap() and other operations).

Here’s a short list of cons of the “modern” approach compared to the classic one:

1. Performance – you will lose on it

Angelika Langer has wrapped up this topic well enough in her article and the related talk that she’s giving at conferences:

Java performance tutorial – How fast are the Java 8 streams?

Note: Her benchmarks have been repeated by Nicolai Parlog with JMH, with slightly different results in the extreme cases, but nothing substantially different:

Stream Performance

Beware, both articles (as well as this one) were written in 2015. Things may have changed for the better, although there is still a measurable difference.

In many cases, performance is not critical, and you shouldn’t do any premature optimisation – so you may claim that this argument is not really an argument per se. But I will counter this attitude in this case, saying that the overhead of Stream.forEach() compared to an ordinary for loop is so significant in general that using it by default will just pile up a lot of useless CPU cycles across all of your application. If we’re talking about 10%-20% more CPU consumption just based on the choice of loop style, then we did something fundamentally wrong. Yes – individual loops don’t matter, but the load on the overall system could have been avoided.

Here’s Angelika’s benchmark result on an ordinary loop, finding the max value in a list of boxed ints:

ArrayList, for-loop : 6.55 ms
ArrayList, seq. stream: 8.33 ms

In other cases, when we’re performing relatively easy calculations on primitive data types, we absolutely SHOULD fall back to the classic for loop (and preferably to arrays, rather than collections).

Here’s Angelika’s benchmark result on an ordinary loop, finding the max value in an array of primitive ints:

int-array, for-loop : 0.36 ms
int-array, seq. stream: 5.35 ms

Such extreme numbers could not be reproduced by Nicolai Parlog or Heinz Kabutz, although a significant difference could still be reproduced.

Premature optimisation is not good, but cargo-culting the avoidance of premature optimisation is even worse. It’s important to reflect on what context we’re in, and to make the right decisions in such a context. We’ve blogged about performance before, see our article Top 10 Easy Performance Optimisations in Java

2. Readability – for most people, at least

We’re software engineers. We’ll always discuss style of our code as if it really mattered. For instance, whitespace, or curly braces.

The reason why we do so is because maintenance of software is hard. Especially of code written by someone else. A long time ago. Who probably wrote only C code before switching to Java.

Sure, in the example we’ve had so far, we don’t really have a readability issue, the two versions are probably equivalent:

List<Integer> list = Arrays.asList(1, 2, 3);

// Old school
for (Integer i : list)

// "Modern"

But what happens here:

List<Integer> list = Arrays.asList(1, 2, 3);

// Old school
for (Integer i : list)
    for (int j = 0; j < i; j++)
        System.out.println(i * j);

// "Modern"
list.forEach(i -> {
    IntStream.range(0, i).forEach(j -> {
        System.out.println(i * j);

Things start getting a bit more interesting and unusual. I’m not saying “worse”. It’s a matter of practice and of habit. And there isn’t a black/white answer to the problem. But if the rest of the code base is imperative (and it probably is), then nesting range declarations and forEach() calls, and lambdas is certainly unusual, generating cognitive friction in the team.

You can construct examples where an imperative approach really feels more awkward than the equivalent functional one, as exposed here:

But in many situations, that’s not true, and writing the functional equivalent of something relatively easy imperative is rather hard (and again, inefficient). An example could be seen on this blog in a previous post:

In that post, we generated a sequence of characters:

A, B, ..., Z, AA, AB, ..., ZZ, AAA

… similar to the columns in MS Excel:

MS Excel column names

The imperative approach (originally by an unnamed user on Stack Overflow):

import static java.lang.Math.*;
private static String getString(int n) {
    char[] buf = new char[(int) floor(log(25 * (n + 1)) / log(26))];
    for (int i = buf.length - 1; i >= 0; i--) {
        buf[i] = (char) ('A' + n % 26);
        n /= 26;
    return new String(buf);

… probably outshines the funcitonal one on a conciseness level:

import java.util.List;
import org.jooq.lambda.Seq;
public class Test {
    public static void main(String[] args) {
        int max = 3;
        List<String> alphabet = Seq
            .rangeClosed('A', 'Z')
        Seq.rangeClosed(1, max)
           .flatMap(length ->
               Seq.rangeClosed(1, length - 1)
                  .foldLeft(Seq.seq(alphabet), (s, i) -> 
                       .map(t -> t.v1 + t.v2)))

And this is already using jOOλ, to simplify writing functional Java.

3. Maintainability

Let’s think again of our previous example. Instead of multiplying values, we divide them now.

List<Integer> list = Arrays.asList(1, 2, 3);

// Old school
for (Integer i : list)
    for (int j = 0; j < i; j++)
        System.out.println(i / j);

// "Modern"
list.forEach(i -> {
    IntStream.range(0, i).forEach(j -> {
        System.out.println(i / j);

Obviously, this is asking for trouble, and we can immediately see the trouble in an exception stack trace.

Old school

Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Test.main(Test.java:13)


Exception in thread "main" java.lang.ArithmeticException: / by zero
	at Test.lambda$1(Test.java:18)
	at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110)
	at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:557)
	at Test.lambda$0(Test.java:17)
	at java.util.Arrays$ArrayList.forEach(Arrays.java:3880)
	at Test.main(Test.java:16)

Wow. Were we just…? Yes. These are the same reasons why we’ve had performance issues in item #1 in the first place. Internal iteration is just a lot more work for the JVM and the libraries. And this is an extremely easy use-case, we could’ve displayed the same thing with the generation of AA, AB, .., ZZ series.

From a maintenance perspective, a functional programming style can be much harder than imperative programming – especially when you blindly mix the two styles in legacy code.


This is usually a pro-functional programming, pro-declarative programming blog. We love lambdas. We love SQL. And combined, they can produce miracles.

But when you migrate to Java 8 and contemplate using a more functional style in your code, beware that FP is not always better – for various reasons. In fact, it is never “better”, it is just different and allows us to reason about problems differently.

We Java developers will need to practice, and come up with an intuitive understanding of when to use FP, and when to stick with OO/imperative. With the right amount of practice, combining both will help us improve our software.

Or, to put it in Uncle Bob’s terms:

The bottom, bottom line here is simply this. OO programming is good, when you know what it is. Functional programming is good when you know what it is. And functional OO programming is also good once you know what it is.