Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

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

DZone's Guide to

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

Moving to Java 8? Congrats! Here's a guide to replacing everything with functions, removing object orientation, and discarding of design patterns.

· Performance Zone
Free Resource

Evolve your approach to Application Performance Monitoring by adopting five best practices that are outlined and explored in this e-book, brought to you in partnership with BMC.

Awesome! We’re migrating our codebase 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 Codebase?

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>() {
        @Override
        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:
http://gafter.blogspot.ch/2007/07/internal-versus-external-iterators.html

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)
    System.out.println(i);

// "Modern"
list.forEach(System.out::println);

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:

https://jaxenter.com/java-performance-tutorial-how-fast-are-the-java-8-streams-118830.html

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

Premature optimisation is not good, but cargo-culting the avoidance of premature optimization 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)
    System.out.println(i);

// "Modern"
list.forEach(System.out::println);

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:


Image title

— Mario Fusco (@mariofusco) March 1, 2015

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:
http://blog.jooq.org/2015/09/09/how-to-use-java-8-functional-programming-to-generate-an-alphabetic-sequence/

In that post, we generated a sequence of characters:

… 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--) {
        n--;
        buf[i] = (char) ('A' + n % 26);
        n /= 26;
    }
    return new String(buf);
}

… probably outshines the functional 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')
            .map(Object::toString)
            .toList();

        Seq.rangeClosed(1, max)
           .flatMap(length ->
               Seq.rangeClosed(1, length - 1)
                  .foldLeft(Seq.seq(alphabet), (s, i) -> 
                      s.crossJoin(Seq.seq(alphabet))
                       .map(t -> t.v1 + t.v2)))
           .forEach(System.out::println);
    }
}

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.

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)

Modern

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.

Conclusion

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.

http://blog.cleancoder.com/uncle-bob/2014/11/24/FPvsOO.html



Learn tips and best practices for optimizing your capacity management strategy with the Market Guide for Capacity Management, brought to you in partnership with BMC.

Topics:
java 8 functional programming ,looping constructs

Published at DZone with permission of Lukas Eder, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

THE DZONE NEWSLETTER

Dev Resources & Solutions Straight to Your Inbox

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.

X

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}