A loop repeats a block of code while a condition holds, or once for each element of a collection. The general idea, and the difference between pre-test and post-test loops, is explained under iteration. The examples on this page are compact source files with void main() and IO.println (see your first program).

while and do-while

A while loop tests its boolean condition before every round and stops as soon as it is false. It suits situations where you do not know in advance how many rounds are needed, such as retrying until something succeeds. Make sure something in the body eventually makes the condition false; otherwise the program hangs in an infinite loop.

void main() {
    int attempts = 0;
    boolean connected = false;

    while (!connected && attempts < 5) {
        attempts++;
        IO.println("Attempt " + attempts + " to reach the meter gateway");
        connected = attempts == 3; // in this demo, the third attempt succeeds
    }
    IO.println(connected ? "Connected" : "Gave up");
}
Attempt 1 to reach the meter gateway
Attempt 2 to reach the meter gateway
Attempt 3 to reach the meter gateway
Connected

The limit of five attempts is a safety net: even if the gateway never answers, the loop ends. That habit matters in real operations code, where an endless retry loop can hammer a system that is already struggling.

do-while

A do…while loop tests its condition after each round, so the body always runs at least once. That fits input validation: you have to ask before you can check the answer. Note the semicolon after the closing while (…).

void main() {
    int kwh;
    do {
        kwh = Integer.parseInt(IO.readln("Meter reading in kWh: "));
        if (kwh < 0) {
            IO.println("A reading cannot be negative.");
        }
    } while (kwh < 0);

    IO.println("Recorded " + kwh + " kWh");
}
Meter reading in kWh: A reading cannot be negative.
Meter reading in kWh: Recorded 412 kWh

The program was given -20 and then 412 as input. IO.readln prints its prompt without a line break, and typed input that comes from a file or pipe is not shown, which is why each message follows the prompt on the same line. In a terminal you would see what you type after the prompt.

The classic for loop

The classic for loop puts the three parts of a counting loop in one line: an initialization that runs once, a condition tested before every round, and an update that runs after every round. It is the tool for working with positions (indexes), for counting with a step, or for walking two arrays side by side.

void main() {
    int[] monthlyKwh = { 412, 388, 395, 351 };
    String[] months = { "January", "February", "March", "April" };

    int total = 0;
    for (int i = 0; i < monthlyKwh.length; i++) {
        IO.println(months[i] + ": " + monthlyKwh[i] + " kWh");
        total += monthlyKwh[i];
    }
    IO.println("Total: " + total + " kWh");

    for (int minutes = 60; minutes > 0; minutes -= 20) {
        IO.println("Maintenance window closes in " + minutes + " minutes");
    }
}
January: 412 kWh
February: 388 kWh
March: 395 kWh
April: 351 kWh
Total: 1546 kWh
Maintenance window closes in 60 minutes
Maintenance window closes in 40 minutes
Maintenance window closes in 20 minutes

Indexes in Java start at 0, so the last valid index of an array is length - 1. Writing <= instead of < is the classic off-by-one error; Java notices it at run time:

void main() {
    int[] monthlyKwh = { 412, 388, 395, 351 };
    int total = 0;
    for (int i = 0; i <= monthlyKwh.length; i++) {
        total += monthlyKwh[i];
    }
    IO.println("Total: " + total);
}
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4

A variable declared in the initialization part, such as i, exists only inside the loop. Using it after the loop is a compile error (cannot find symbol); declare it before the loop if you need its final value. Arrays themselves are covered on arrays and collections.

The enhanced for loop

Most loops do not need an index: they simply visit every element. The enhanced for loop (often called the for-each loop) does exactly that: for (Type name : source) runs the body once for each element of source, which can be an array or anything that is Iterable, such as a List or a Set. Read the colon as "in".

void main() {
    List<String> openTickets = List.of("INC-10041", "INC-10042", "INC-10045");
    for (String ticket : openTickets) {
        IO.println("Reminder sent for " + ticket);
    }

    double[] temperatures = { 18.5, 21.0, 19.5, 21.0 };
    double sum = 0;
    for (double t : temperatures) {
        sum += t;
    }
    IO.println("Average: " + sum / temperatures.length + " °C");
}
Reminder sent for INC-10041
Reminder sent for INC-10042
Reminder sent for INC-10045
Average: 20.0 °C

There is no index and no condition to get wrong, which is why the enhanced for loop is the default choice. Use the classic form only when you really need the position, need to walk backwards or in steps, or need to replace elements of an array.

The loop variable is a copy

The loop variable receives a copy of each element's value. Assigning a new value to it changes only that copy, never the array or list:

void main() {
    int[] readings = { 412, -1, 395 };
    for (int reading : readings) {
        if (reading < 0) {
            reading = 0; // changes the copy only
        }
    }
    IO.println(Arrays.toString(readings));

    for (int i = 0; i < readings.length; i++) {
        if (readings[i] < 0) {
            readings[i] = 0; // changes the array
        }
    }
    IO.println(Arrays.toString(readings));
}
[412, -1, 395]
[412, 0, 395]

When the elements are objects, the copy is a copy of the reference, so calling a method that changes the object does affect the object in the list. The same rule applies to method parameters, as explained under methods and parameters.

Iterating over a map

A Map is not Iterable itself. Loop over one of its views instead: keySet(), values() or entrySet(), which gives you the key and the value together. A TreeMap keeps its keys sorted, so the order of the output is predictable; a HashMap makes no promise about order (see maps).

void main() {
    Map<String, Integer> usageByMeter = new TreeMap<>();
    usageByMeter.put("E-002", 1500);
    usageByMeter.put("E-001", 350);
    usageByMeter.put("G-007", 12);

    for (Map.Entry<String, Integer> entry : usageByMeter.entrySet()) {
        IO.println(entry.getKey() + " used " + entry.getValue());
    }
}
E-001 used 350
E-002 used 1500
G-007 used 12

for (var entry : usageByMeter.entrySet()) is a shorter way to write the same loop; var lets the compiler infer the long type (see variables, types and operators).

break and continue

break leaves the loop immediately; the program continues after it. continue skips the rest of the current round and starts the next one (in a for loop, after running the update). Both are useful, but a loop with many of them becomes hard to follow, so use them for clear exits such as "found it" or "skip this one".

void main() {
    List<String> queue = List.of(
            "INC-10041 P3 resolved",
            "INC-10042 P2 open",
            "INC-10043 P1 open",
            "INC-10044 P1 open");

    for (String line : queue) {
        if (line.endsWith("resolved")) {
            continue; // nothing to do for this one
        }
        IO.println("Checking " + line);
        if (line.contains(" P1 ")) {
            IO.println("First P1 found: " + line.substring(0, 9));
            break; // stop searching
        }
    }
}
Checking INC-10042 P2 open
Checking INC-10043 P1 open
First P1 found: INC-10043

The loop stopped at the first P1 ticket; INC-10044 was never checked. A while (true) loop with a break inside is a common pattern when the exit condition is easiest to test in the middle of the body.

Labeled break and continue

In nested loops, a plain break or continue only affects the innermost loop. To leave the outer loop as well, give it a label (a name followed by a colon) and write break label;. This labeled break jumps out of the labeled loop in one step; continue label; likewise starts the next round of the outer loop. The concept is the same as JavaScript's labeled statements.

void main() {
    String[] substations = { "Delft", "Gouda", "Leiden" };
    double[][] transformerTemps = {
            { 61.0, 64.5 },
            { 58.0, 91.5, 63.0 },
            { 95.0, 60.0 }
    };

    search:
    for (int s = 0; s < substations.length; s++) {
        for (int t = 0; t < transformerTemps[s].length; t++) {
            if (transformerTemps[s][t] > 90) {
                IO.println("Overheating: " + substations[s] + ", transformer " + (t + 1));
                break search;
            }
        }
        IO.println(substations[s] + " is fine");
    }
}
Delft is fine
Overheating: Gouda, transformer 2

Leiden is never inspected and "Gouda is fine" is never printed: break search left both loops at once. Without the label, only the inner loop would stop, and the outer loop would carry on. If you find yourself reaching for labels often, moving the nested loops into a separate method and using return is usually easier to read.

Iterators

Behind every enhanced for loop over a collection is an iterator: an object that walks through a collection one element at a time. Its hasNext() method says whether another element is left, next() returns it and moves on, and the optional remove() deletes the element that next() returned last. Every class that implements Iterable (the Java form of an iterable) can hand out an iterator, which is all the enhanced for loop needs.

void main() {
    List<String> team = List.of("Amara", "Mei", "Hamid");

    Iterator<String> it = team.iterator();
    while (it.hasNext()) {
        String name = it.next();
        IO.println("On shift: " + name);
    }
}
On shift: Amara
On shift: Mei
On shift: Hamid

The compiler turns for (String name : team) into almost exactly this code. You rarely write it by hand, except when you need remove(), as in the next section.

Changing a collection while you loop

Removing or adding elements to a list while an enhanced for loop runs over it breaks the iterator's bookkeeping. Java's standard collections detect this and throw a ConcurrentModificationException, despite the name usually in a program with a single thread. The check happens in next(), so the exception appears at the start of the round after the change.

void main() {
    List<String> tickets = new ArrayList<>(
            List.of("INC-10041 resolved", "INC-10042 open", "INC-10043 open"));

    for (String ticket : tickets) {
        if (ticket.endsWith("resolved")) {
            tickets.remove(ticket);
        }
    }
    IO.println(tickets);
}
Exception in thread "main" java.util.ConcurrentModificationException

The check is "best effort", and that makes the bug sneaky. When the element you remove is the second-to-last one, the loop simply ends without an exception, and the last element is never visited:

void main() {
    List<String> team = new ArrayList<>(List.of("Amara", "Mei", "Hamid"));

    for (String name : team) {
        IO.println("Visiting " + name);
        if (name.equals("Mei")) {
            team.remove(name);
        }
    }
    IO.println(team);
}
Visiting Amara
Visiting Mei
[Amara, Hamid]

After the removal the list has two elements and the iterator has already handed out two, so hasNext() answers false. Hamid is still in the list but was skipped. A test with a different list would throw instead, which is why such code can pass testing and fail later.

Removing safely

There are two safe ways. removeIf takes a condition (a lambda) and removes every matching element in one call. When you need more logic per element, use an explicit iterator and its remove() method, which keeps the bookkeeping right.

void main() {
    List<String> tickets = new ArrayList<>(
            List.of("INC-10041 resolved", "INC-10042 open", "INC-10043 resolved"));
    tickets.removeIf(ticket -> ticket.endsWith("resolved"));
    IO.println(tickets);

    List<String> team = new ArrayList<>(List.of("Amara", "Mei", "Hamid"));
    Iterator<String> it = team.iterator();
    while (it.hasNext()) {
        String name = it.next();
        if (name.startsWith("M")) {
            it.remove();
            IO.println("Removed " + name);
        }
    }
    IO.println(team);
}
[INC-10042 open]
Removed Mei
[Amara, Hamid]

Unnamed variables

Sometimes a loop or a catch needs a variable that you never use, for instance when you only count elements. Since Java 22 you can write an underscore instead of a name. An unnamed variable _ tells both the reader and the compiler that the value is deliberately ignored.

void main() {
    List<String> rawReadings = List.of("412", "38x", "395", "", "351");

    int lines = 0;
    for (String _ : rawReadings) {
        lines++;
    }

    int valid = 0;
    int total = 0;
    for (String raw : rawReadings) {
        try {
            total += Integer.parseInt(raw);
            valid++;
        } catch (NumberFormatException _) {
            // skip readings that are not numbers
        }
    }
    IO.println(valid + " of " + lines + " readings valid, total " + total + " kWh");
}
3 of 5 readings valid, total 1158 kWh

The try and catch blocks are explained on exceptions and error handling. Several unnamed variables may appear in the same scope, because none of them can be referred to.

Loops and streams

Modern Java code often replaces simple loops with a method call. A collection's forEach method runs a lambda for every element, IntStream.rangeClosed(1, 12) produces the numbers 1 to 12, and a stream pipeline can filter, transform and add up in one expression.

void main() {
    List<Integer> monthlyKwh = List.of(412, 388, 395, 351, 298, 260);

    int loopTotal = 0;
    for (int kwh : monthlyKwh) {
        if (kwh > 300) {
            loopTotal += kwh;
        }
    }

    int streamTotal = monthlyKwh.stream()
            .filter(kwh -> kwh > 300)
            .mapToInt(Integer::intValue)
            .sum();

    IO.println(loopTotal + " " + streamTotal);
    IO.println(IntStream.rangeClosed(1, 5).sum());
}
1546 1546
15

Both versions give the same answer; choose the one that reads best. Streams shine when there are several steps (filter, map, group, sort); a plain loop is clearer when the body has side effects, needs break, or changes several variables. Streams are covered on lambdas, functional interfaces and streams.

Choosing a loop

Loop Use it when
for (T x : items) you visit every element of an array or collection (the default)
for (int i = 0; i < n; i++) you need the index, a step, a reverse order, or you change array elements
while (condition) the number of rounds is unknown: retries, reading until the input ends
do { … } while (condition); the body must run at least once, as with asking for input
explicit Iterator you remove elements while walking through a collection
removeIf, forEach, streams one clear operation on a whole collection: remove, print, filter, sum

Whatever the loop, think about how much work it does as the data grows: a loop inside a loop over the same list grows with the square of its size (see nested loops over the same data).