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.
for (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
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.
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 3Removing 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.
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.
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