Skip to content
Free public beta until December 4, 2026: every course is open, no card needed. After that, the first chapter of each course stays free and full access is a one-time $39 ($19 for up to 100 founding members chosen for helpful feedback). Your progress is kept. How the beta works

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 every Collection, 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, or do-while when the body must run at least once.
  • Remove some elements while you go → list.removeIf(...), or an explicit Iterator and it.remove(). Never list.remove inside an enhanced for: that throws ConcurrentModificationException.
  • 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

The counting loop with an index, and the enhanced for that visits every element.

A loop that visits every item of a group in order, written for (String word : args).

Example

for (int score : scores) total += score;

No index, no off-by-one: the default for reading every element.

When the number of passes isn't known in advance.

Leaving early, skipping a pass, and getting the bounds right.

Sum, count, max, search: the loop shapes every later chapter reuses.

Changing the collection while you loop

Why removing inside an enhanced for fails, and the safe ways: removeIf and Iterator.remove.

An object that walks through a collection one element at a time with hasNext and next.

The object an enhanced for uses behind the scenes; its own remove is the safe one.

The exception an iterator throws when its collection was changed by something other than the iterator.

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

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.

When a stream reads better than the loop, and when to keep the loop.