Java – Usage of For-each loop vs functional operation vs Java8 streams

javajava8stream-processing

What are the gains in actual computing speed (verbosity of code being put aside) and for which case would each be recommended for, as a good design pattern?
I would like to know the above in general but also if it helps in the context of the following which can re-written in all 3 ways

static void parallelTestsExecution(String name,Runnable ... runnables){
    ArrayList<BaseTestThread> list = new ArrayList();
    int counter =0; // use local variable counter to name threads accordingly
    for(Runnable runnable : runnables){//runnable contains test logic
        counter++;
        BaseTestThread t = new BaseTestThread(runnable);
        t.setName(name+" #"+counter);
        list.add(t);
    }

    for(BaseTestThread thread : list ){//must first start all threads
        thread.start();
    }

    for(BaseTestThread thread : list ){//start joins only after all have started
        try {
            thread.join();
        } catch (InterruptedException ex) {
            throw new RuntimeException("Interrupted - This should normally not happen.");
        }
    }

    for(BaseTestThread thread : list ){
        assertTrue("Thread: "+thread.getName() , thread.isSuccessfull());
    }

}

vs

 static void parallelTestsExecution(String name,Runnable ... runnables){
    ArrayList<BaseTestThread> list = new ArrayList();
    int counter =0; // use local variable counter to name threads accordingly
    for(Runnable runnable : runnables){
        counter++;
        BaseTestThread t = new BaseTestThread(runnable);//BaseTestThread extends thread
        t.setName(name+" #"+counter);
        list.add(t);
    }

    list.forEach((thread) -> {
        thread.start();
    });

    list.forEach((thread) -> {
        try {
            thread.join();
        } catch (InterruptedException ex) {
            throw new RuntimeException("Interrupted - This should normally not happen.");
        }
    });

    list.forEach((thread) -> {
        assertTrue("Thread: "+thread.getName() , thread.isSuccessfull());
    });

}

vs
Populating the list with threads each associated with a runnable and using
list.Stream()… etc (currently not confident enough with this one to post something more concrete)

I am specifically interested in learning how each of those different mechanisms work and what are their strength/weaknesses to apply my better judgment whenever its needed to decide what to use as a best practice. Question isnt about micro-optimisation in general and the importance of it.

Best Answer

Performance

In your example, I can hardly imagine any significant performance difference between the different styles, as in both cases, the work is done in N parallel Threads. Creating and managing the threads will vastly dominate the run time, even if the threads themself do nothing.

The different styles only apply in how you create the threads, start the threads, and wait for the threads to finish.

Similarities

Both styles have the basic way of iterating in common. Both the Java5 loop style and the (default implementation of the) Java8 lambda style use the iterator() of the array/list/collection and its next() and hasNext() methods.

Differences

What's different is the way the body gets executed.

The Java5 style loop immediately uses your code as the loop body (inside your parallelTestsExecution() method), while the Java8 lambda style encapsulates the functionality into the accept() method of an anonymous Acceptor object that gets called for each iteration. [Have a look at the class files created from your source. With the second style, there will be some xxx$1 and similar classes for the aforementioned Acceptors.]

So, there is an overhead when using the Java8 lambda style, but I guess the Hotspot compiler can optimize this to run as fast as a normal loop.

But this style affects the accessibility of local variables residing outside of the loop body. With Java5 loops, they are fully accessible, read and write, because they are in the same method. With Java8 lambdas, you can only read locals, and only if they are declared final [* see below] - your loop body resides in a different class, and accessing variables from your parallelTestsExecution() method is impossible for the JVM. The trick that the compiler uses, is to pass the value of your variable into the hidden constructor of the anonymous Acceptor class, and have the Acceptor class store this copy as an instance field. So, you can't transform your first loop to lambda style because you modify the "counter" variable inside which is impossible from an anonymous class.

It also affects debugging. Suppose you set a breakpoint at your line "thread.start();". In the first approach, you'll find yourself immediately in your parallelTestsExecution() method. With lambdas, you'll see an accept() method inside a forEach() method inside your parallelTestsExecution() method, maybe with some more intermediate glue.

[*] Remark on "final": As Jules correctly commented, the "final" declaration is no longer necessary. If you use a local variable inside of a lambda, it still has to be effectively final. That doesn't give you any more possibilities, just saves you typing the word "final".