How to Iterate with Java
Iterating data structures is one of the most common tasks.
Everyone knows the classics, like for
or while
. But there are more ways to iterate in Java, providing a lot more functionality.
iteration
noun it·er·a·tion | \ ˌi-tə-ˈrā-shən \one execution of a sequence of operations or instructions in an iteration
— Merriam-Webster
Table of Contents
The Classics
There are 3 classic ways to iterate with language-integrated keywords:
for
- Enhanced
for
(Java 5+) while
/do-while
The for
-loop
A for
-loop is defined as:
The 3 expressions in the parentheses have different life cycles:
- Initialization
- Called once at the beginning of the loop
- Termination
- Terminates the loop if it evaluates to
false
- Increment
- Invoked after every iteration
Usually, all 3 expressions are used, but they aren’t required:
Even though it’s possible to write code like this, I wouldn’t recommend it.
The for
-loop is a great tool to confine the iteration to a single place by putting the 3 expressions in a single pair of parentheses.
If we split up the expressions, our code becomes harder to reason with.
Without a termination expression, the loop would run endlessly:
We would need to use break
inside the loop to terminate it.
Endless loops are often used for a technique called busy waiting, which is considered an anti-pattern. There are many other ways available to us to deal with concurrency, waiting for threads, etc., but these are beyond the scope of this article.
Enhanced for-loop
The traditional expression-based for
-loop does its work quite fine, but it is also a noisy and verbose construct. Most of the time, our loops actually start with the same logic:
Iterate over all items and access the current item
Java 5 introduced the enhanced for
-loop, to simplify this common task.
It’s available to data structures that conform to java.util.Iterable<T>
or are an array.
Every array has Object
as its base class, and implements Serializable
and Cloneable
, but not Iterable
.
One of the main advantages is the ability to store primitive types (e.g., int
, long
) and not just full-fledged objects, so we don’t have to rely on auto-boxing.
The syntax is simple:
Our previous example becomes quite more reasonable:
Usually, the actual type is clear thanks to the immediate context.
So using var
will reduce any additional noise from longer type names.
while / do-while
The while
-loop can be seen as a simplified for
-loop with only a termination expression:
As with for
, the termination expression breaks the loop if it evaluates to false
.
There are scenarios where a possible termination needs to be evaluated after the loop-block.
That’s what do-while
is doing:
This way, the loop-block is run at least once, regardless of the termination expression.
Iterator-based
Iterable
The interface java.util.Iterable<E>
makes a data structure usable for the enhanced for
-loop by providing a java.util.Iterator<E>
:
It’s a simple abstraction of a data structure by knowing if there is another element and how to get it. The surrounding frame for the actual iteration is usually provided by a loop:
That’s quite noisy compared to an enhanced for
-loop, so what’s the advantage?
The Iterator#remove()
method makes all the difference.
While iterating, we can’t change the data we’re currently iterating over.
This code will throw a java.util.ConcurrentModificationException
:
Actually, the ConcurrentModificationException
isn’t thrown during the call to List#remove
(Object) in this case.
Instead, the Iterator<Integer>
accessing next()
will throw the exception.
With the help of using an Iterator directly, we can remove while iterating:
We could also use a for
-loop instead, and combine initialization and termination into a single line:
ListIterator
Being an Iterator<E>
at heart, the ListIterator<E>
provides additional functionality to navigate backward, and for modification:
A ListIterator
can be visualized as being between elements:
Any modification operations will be performed on the last returned element by next()
or previous()
.
Lambda-based
The introduction of lambdas with Java 8 brought new possibilities to iterate.
Thanks to default methods, every type implementing Iterable<T>
gains the ability to apply a Consumer<T>
to every element:
As we can see by its simple implementation, we won’t gain much compared to a normal enhanced for
-loop.
That’s why it’s best for simple use cases, like calling a method reference:
forEach with Streams
Streams are lazy-sequential data pipelines of functional blocks, which means they will iterate over the data, applying the functional blocks appropriately.
In addition to iterating with the stream’s intermediate operations itself, we can also use the terminal operation Stream#forEach(Consumer<? super T> action)
.
But just like with Iterable
, smaller and more reasonable blocks improve our code’s general clarity.
It’s better to move as much logic as possible to the intermediate operations, and as high up as possible, so the total count of operations is as small as possible:
Lambda vs. Traditional
We now have learned about different ways to iterate over data structures.
But how do we choose which to use?
As much as I love lambdas, they come with multiple downsides, especially regarding iteration:
Exceptions: Handling (checked) exceptions isn’t fun with lambdas, I wrote a whole article on how to deal with them in a functional manner.
No breaks: A
return
in a lambda will function like acontinue
in a traditional loop, but there’s no equivalent forbreak
.JVM optimizations: No loop-unrolling. This doesn’t mean there are no optimizations of lambdas, but they are different compared to traditional loops.
Deeper callstack: A stack frame will be created for the additional method call.
Debugging: Even though IDEs got better at handling lambdas and streams, it’s still simpler to step through a loop. Especially with the additional stack frame.
No side effects: Only effectively
final
variables can be used in the lambda.
At first read, that might sound really bad. But lambdas and streams also offer many advantages:
Parallel execution: Streams can be parallelized without needing an additional
ExecutorService
.Fluency: A fluent call can split up operations into more manageable blocks. The whole call can be more concise and explicit compared to a loop-block.
No side effects: Less side effects due to effectively
final
variables.
Conclusion
As you see, there’s no simple answer to use which type of iteration. It all depends on the context. Code readability and maintainability should play a big role in deciding which kind of iteration fits best.
Resources
- The For-Each Loop (Oracle)
- The List Interface (Oracle)
- The for Statement (JLS)