Every way to loop over a collection
Index for, enhanced for, while, an Iterator, removeIf, forEach and a stream: what each one can do, and which to reach for.
Start with what the loop has to do with each element.
- Read every element, in order → the enhanced
for:for (String name : names). It works on arrays and on everyCollection, and it can't run off the end. - You need the position too (compare with the neighbour, fill a second array) → the classic
for (int i = 0; i < a.length; i++). - You don't know in advance when to stop (read until a sentinel, halve until small enough) →
while, ordo-whilewhen the body must run at least once. - Remove some elements while you go →
list.removeIf(...), or an explicitIteratorandit.remove(). Neverlist.removeinside an enhancedfor: that throwsConcurrentModificationException. - Build one result (a filtered list, a total, a grouped map) → a stream pipeline, if it reads better than the loop. Keep the loop when it needs
break, checked exceptions or several results at once.
Loops you write yourself
Section
for and enhanced for
The counting loop with an index, and the enhanced for that visits every element.
Example
for (int score : scores) total += score;No index, no off-by-one: the default for reading every element.
Section
while and do-while
When the number of passes isn't known in advance.
Section
break, continue and loop design
Leaving early, skipping a pass, and getting the bounds right.
Section
Array traversal patterns
Sum, count, max, search: the loop shapes every later chapter reuses.
Changing the collection while you loop
Section
Iteration and modification
Why removing inside an enhanced for fails, and the safe ways: removeIf and Iterator.remove.
Term
iterator
The object an enhanced for uses behind the scenes; its own remove is the safe one.
Example
for (String s : list) if (s.isBlank()) list.remove(s); throws it.What an iterator throws when the collection changed under it.
Letting the library loop for you
Section
Stream pipelines
Source, intermediate steps, one terminal operation.
Formal. A stream pipeline is a source, zero or more intermediate
operations (filter, map, sorted, distinct, limit, skip), and
one terminal operation (toList, collect, count, forEach,
reduce, findFirst). Intermediate operations only record a step and
return a new stream; nothing is read from the source until the terminal
operation runs. A stream can be consumed once: reusing it throws
IllegalStateException. A stream never modifies its source.
In plain English. Writing the steps of a pipeline is like writing a recipe: nothing is cooked until the last line says "serve". And each stream is a single pass over the data: to go again, ask the collection for a new stream.
Example. orders.stream().filter(o -> o.total() > 100) on its own
does nothing. Adding .count() runs it. Calling count() twice on the
same stream object throws IllegalStateException: stream has already been operated upon or closed.
Why this rule exists. Laziness lets the JVM pass each element
through every step before reading the next one, and lets limit and
findFirst stop early, even on an endless source.
Nothing runs until the terminal operation, and a stream is single-use.
Section
Loops or streams?
When a stream reads better than the loop, and when to keep the loop.