Polyglot
Languages Java loops
Java § loops

Loops

Java provides four loop forms: the C-style for, the enhanced for (Java 5, often called for-each), while, and do … while. The enhanced for is the conventional form for iterating over arrays and any type that implements Iterable<T>; the C-style for retains its place where the index is genuinely needed or where the iteration is non-trivial. The streams API (Java 8) provides a substantial alternative to explicit loops for many filter-map-reduce patterns; the choice between explicit loops and stream pipelines is one of the principal style decisions in modern Java.

while

The while loop tests its controlling expression before each iteration:

while (!queue.isEmpty()) {
    var item = queue.poll();
    process(item);
}

The construct is the appropriate one for iteration whose termination condition is checked at the start, and where the number of iterations is not known in advance. Reading from a stream, polling for a state change, and graph traversal are typical.

The controlling expression must be of type boolean (no implicit conversion from integer or reference):

while (input != null && hasMoreData(input)) {
    /* ... */
}

do … while

The do … while loop tests its controlling expression after each iteration; the body always executes at least once:

int input;
do {
    System.out.print("> ");
    input = scanner.nextInt();
} while (input < 0 || input > 100);

The construct is appropriate for iterations whose first execution is unconditional. It is less common than while in idiomatic Java; the principal uses are input prompts and certain numerical methods.

The terminating semicolon after while (cond) is a syntactic peculiarity; it is the only compound statement in Java that requires one.

The C-style for

The C-style for combines initialisation, test, and step:

for (int i = 0; i < n; i++) {
    process(data[i]);
}

The header has three parts:

  1. The init-statement (an expression statement or a declaration), evaluated once before the loop.
  2. The condition, evaluated before each iteration; the loop terminates when it is false.
  3. The iteration-expression, evaluated after each iteration.

Any of the three may be omitted; for (;;) is the conventional infinite loop:

for (;;) {
    var event = queue.take();
    if (event.isQuit()) break;
    handle(event);
}

The init declaration scopes the variable to the loop:

for (int i = 0; i < n; i++) {
    /* i is in scope */
}
// i is not in scope here

The C-style for is conventional in Java for index-based iteration (when the index matters) and for non-trivial step patterns. For “iterate over a collection” the conventional choice is the enhanced for.

Multi-variable for

The init-clause may declare multiple variables of the same type, and the iteration-expression may have multiple comma-separated expressions:

for (int i = 0, j = n - 1; i < j; i++, j--) {
    swap(arr, i, j);
}

The form is occasionally useful for two-pointer or reverse-iteration patterns.

The enhanced for (for-each)

Java 5 introduced the enhanced for, the conventional loop for iterating over arrays and any type that implements Iterable<T>:

List<Integer> values = List.of(1, 2, 3, 4, 5);

for (int v : values) {
    System.out.println(v);
}

int[] arr = { 1, 2, 3 };
for (int x : arr) {
    System.out.println(x);
}

Map<String, Integer> ages = Map.of("alice", 30, "bob", 28);
for (var entry : ages.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

The element variable’s type may be specified explicitly (int v) or inferred via var (Java 10+):

for (var person : people)         { /* person is Person */ }
for (var entry : map.entrySet())  { /* entry is Map.Entry<K, V> */ }

The iterable may be any expression that yields an Iterable<T>, an array, or (for the limited primitive types) a primitive array. Maps require explicit .entrySet(), .keySet(), or .values() because Map<K, V> does not directly implement Iterable.

Modification during iteration

A enhanced for over a List<T> may not modify the underlying list during the iteration; doing so throws ConcurrentModificationException on the next step:

for (Item item : items) {
    if (item.isExpired()) {
        items.remove(item);          // throws ConcurrentModificationException
    }
}

The conventional alternatives:

  • Use Iterator<T>.remove():
Iterator<Item> it = items.iterator();
while (it.hasNext()) {
    Item item = it.next();
    if (item.isExpired()) it.remove();
}
  • Use Collection.removeIf (Java 8+):
items.removeIf(Item::isExpired);
  • Use a stream and collect to a new list:
items = items.stream().filter(item -> !item.isExpired()).collect(Collectors.toList());

The removeIf form is the conventional contemporary choice for “remove all elements matching a predicate”.

break and continue

break exits the innermost enclosing for, while, do, or switch:

for (Item item : items) {
    if (item.matches(target)) {
        found = item;
        break;
    }
}

continue skips the rest of the current iteration and proceeds to the next test:

for (Item item : items) {
    if (item.isHidden()) continue;
    process(item);
}

Java has labelled break and continue for nested loops:

outer:
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        if (matrix[i][j] == target) {
            row = i; col = j;
            break outer;        // exits both loops
        }
    }
}

The label refers to the loop the statement should affect. break outer exits the labelled loop; continue outer skips to the next iteration of the labelled loop. The mechanism is rare in modern code; refactoring into a method whose return exits both is often clearer.

Iterators and Iterable

A type that wishes to support enhanced for implements Iterable<T>:

public interface Iterable<T> {
    Iterator<T> iterator();
}

Iterator<T> declares three methods:

public interface Iterator<E> {
    boolean hasNext();
    E       next();
    default void remove() { throw new UnsupportedOperationException(); }
}

A user-defined iterable:

public class Range implements Iterable<Integer> {
    private final int from, toExclusive;

    public Range(int from, int toExclusive) {
        this.from = from;
        this.toExclusive = toExclusive;
    }

    @Override
    public Iterator<Integer> iterator() {
        return new Iterator<>() {
            private int current = from;

            @Override
            public boolean hasNext() { return current < toExclusive; }

            @Override
            public Integer next() {
                if (!hasNext()) throw new NoSuchElementException();
                return current++;
            }
        };
    }
}

for (int i : new Range(0, 5)) {
    System.out.println(i);     // 0, 1, 2, 3, 4
}

The mechanism is the conventional way to make a custom collection or sequence iterable. Modern Java often uses streams (Stream<T>) or the IntStream/LongStream/DoubleStream primitives for the same purpose.

forEach and method references

Every Iterable<T> admits the forEach default method (Java 8):

items.forEach(item -> process(item));
items.forEach(System.out::println);

The pattern is the conventional alternative when the body is short and side-effecting; for substantial bodies, the explicit for form is clearer.

Streams as an alternative

A substantial fraction of explicit loops in Java code can be replaced by stream pipelines:

// Explicit:
int total = 0;
for (Order o : orders) {
    if (o.isActive()) total += o.amount();
}

// Stream:
int total = orders.stream()
    .filter(Order::isActive)
    .mapToInt(Order::amount)
    .sum();

The full treatment is in Streams. The conventional contemporary advice:

  • Use enhanced for for ordinary iteration where the body has side effects or is non-trivial.
  • Use streams for filter, map, reduce, group, and sort operations.
  • Use the C-style for only when the index is genuinely needed or when iterating two collections in parallel.
  • Use iterators for custom sequences that should be lazy.

Common iteration patterns

Index and value together

Java does not provide a built-in enumerate (such as Python’s). The conventional substitutes:

// With index:
for (int i = 0; i < items.size(); i++) {
    process(i, items.get(i));
}

// With stream and IntStream.range:
IntStream.range(0, items.size())
    .forEach(i -> process(i, items.get(i)));

Iterating two collections in parallel

Iterator<A> ait = a.iterator();
Iterator<B> bit = b.iterator();
while (ait.hasNext() && bit.hasNext()) {
    process(ait.next(), bit.next());
}

Or with streams:

IntStream.range(0, Math.min(a.size(), b.size()))
    .forEach(i -> process(a.get(i), b.get(i)));

There is no built-in zip operation in the streams API; third-party libraries (Guava, jOOλ) provide one.

Reading lines from a file

try (var reader = Files.newBufferedReader(path)) {
    String line;
    while ((line = reader.readLine()) != null) {
        process(line);
    }
}

// Or with streams (Java 8+):
try (var lines = Files.lines(path)) {
    lines.forEach(this::process);
}

Files.lines is the conventional contemporary choice; the returned Stream<String> is AutoCloseable and must be closed.

Iterating a map’s entries, keys, or values

for (var entry : map.entrySet()) {
    System.out.println(entry.getKey() + ": " + entry.getValue());
}

for (var key : map.keySet())   { /* keys */ }
for (var val : map.values())   { /* values */ }

Reverse iteration

for (int i = items.size() - 1; i >= 0; i--) {
    process(items.get(i));
}

// Or with Collections.reverse (mutates):
var reversed = new ArrayList<>(items);
Collections.reverse(reversed);
for (var item : reversed) { process(item); }

There is no for-each form for reverse iteration; the explicit index loop is the conventional answer.