for Loops

Use the classic counting for loop when you need the index, a step size or a range, and avoid the off-by-one and remove-while-looping bugs.

Syntaxfor (int i = 0; i < n; i++) { ... } for (int start = 0; start < n; start += batchSize) { ... }

The classic for loop has three parts: an initialiser that runs once, a condition checked before every pass, and an update that runs after every pass.

for (int i = 0; i < items.size(); i++) {
    // i is 0, 1, ... size - 1
}

The variable i exists only inside the loop. If you need it afterwards, for example to know where a search stopped, declare it before the loop.

When to use it

If you only need each element, the enhanced for loop (for (Order o : orders)) or a stream is shorter and cannot go out of bounds. Reach for the counting loop when the index itself matters:

  • stepping in chunks, such as sending rows to the database in batches of 500;
  • walking backwards, or comparing an element with its neighbour;
  • writing into an array by position;
  • looping over something that is not a collection, such as a range of dates or retry attempts.

Any part can use any type. for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) is a perfectly good loop over a date range. You can declare two variables of the same type and update both: for (int i = 0, j = n - 1; i < j; i++, j--).

Off-by-one errors

Indexes run from 0 to length - 1, so the condition is i < length. Writing i <= length runs one extra pass and throws ArrayIndexOutOfBoundsException (or IndexOutOfBoundsException for a list). When you slice a list into batches, clamp the end with Math.min(start + size, list.size()) so the final, shorter batch is not lost or overrun.

Removing while looping

Removing element i from an ArrayList shifts everything after it one place left. A forward loop then increments i and skips the element that moved into the gap. Either loop backwards from size() - 1, or skip the loop and use list.removeIf(...), which is clearer.

Watch the condition

The condition is evaluated every pass. list.size() is cheap, but a database call or a method that builds a new collection is not; compute it once before the loop. An update that never moves towards the exit, such as i-- in a loop counting up, gives an infinite loop that keeps a request thread busy for good: a request timeout only stops the caller waiting, it does not stop the thread.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Send rows to the database in fixed-size batches with a start index that steps by the batch size.
  • Build a daily report that has one row for every date in a requested range.
  • Walk a list backwards to remove expired entries by index without skipping any.

More examples

The off-by-one that runs past the end

The last valid index is length - 1, so the condition must be <. The exception message names the bad index and the length, which makes this bug quick to spot in logs.

Example · java
void main() {
    String[] regions = {"eu-west", "us-east", "ap-south"};

    for (int i = 0; i < regions.length; i++) {
        IO.println(i + ": " + regions[i]);
    }

    try {
        for (int i = 0; i <= regions.length; i++) { // <= is one pass too many
            String region = regions[i];
        }
    } catch (ArrayIndexOutOfBoundsException e) {
        IO.println(e.getMessage());
    }
}
// Output:
// 0: eu-west
// 1: us-east
// 2: ap-south
// Index 3 out of bounds for length 3

Removing by index: forwards skips, backwards works

After removing 500 the value 503 moves into its index, and the forward loop has already moved on. Walking backwards only shifts elements that have been checked; removeIf avoids the question altogether.

Example · java
void main() {
    List<Integer> forwards = new ArrayList<>(List.of(200, 500, 503, 201, 502));
    for (int i = 0; i < forwards.size(); i++) {
        if (forwards.get(i) >= 500) {
            forwards.remove(i); // the next element slides into i and is skipped
        }
    }
    IO.println("forwards:  " + forwards);

    List<Integer> backwards = new ArrayList<>(List.of(200, 500, 503, 201, 502));
    for (int i = backwards.size() - 1; i >= 0; i--) {
        if (backwards.get(i) >= 500) {
            backwards.remove(i);
        }
    }
    IO.println("backwards: " + backwards);

    List<Integer> simple = new ArrayList<>(List.of(200, 500, 503, 201, 502));
    simple.removeIf(code -> code >= 500);
    IO.println("removeIf:  " + simple);
}
// Output:
// forwards:  [200, 503, 201]
// backwards: [200, 201]
// removeIf:  [200, 201]

Looping over a date range

The loop variable can be any type, as long as the update moves it towards the exit. This is how a reporting endpoint fills in a row for every day, including days with no data.

Example · java
void main() {
    LocalDate start = LocalDate.of(2026, 3, 30);
    LocalDate end = LocalDate.of(2026, 4, 2);

    // Inclusive range: keep going until the date is after the end.
    for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) {
        IO.println(day + " " + day.getDayOfWeek());
    }
}
// Output:
// 2026-03-30 MONDAY
// 2026-03-31 TUESDAY
// 2026-04-01 WEDNESDAY
// 2026-04-02 THURSDAY

Discussion

  • Be the first to comment on this lesson.