skip to content

In a Java for loop, what is the scope of a counter declared in the header, and what common off-by-one and update pitfalls should you watch for?

level: seniorimportance: should knowfreq 45%

answer

  1. header-declared counter is loop-scoped (gone after)
  2. < for exclusive bound (0..n-1), <= for inclusive
  3. don't also mutate the counter in the body
  4. mutating a list by index mid-loop skips/overruns
  5. beware recomputed bounds, int overflow, stray ; empty body

basics

~20 s

A counter declared in the for header only exists inside the loop. Common bugs are using < vs <= wrongly (off-by-one), modifying the counter inside the body, and changing the collection size while looping by index.

solid answer

~50 s

A variable declared in the for initialization clause is scoped to the loop header and body only; after the loop it is undefined, which prevents accidental reuse. The classic pitfalls: off-by-one errors from choosing < versus <= (for indices 0..n-1 you want i < n, not i <= n, which throws an IndexOutOfBoundsException), mutating the counter inside the body in addition to the update clause (making the loop count unpredictable), and changing a collection's size while iterating it by index, which skips elements or overruns. Other traps include comparing the counter against a value recomputed every pass (e.g. list.size() shrinking), accidental int overflow when the bound is near Integer.MAX_VALUE turning into an infinite loop, and an empty body created by a stray semicolon after the header. To stay safe, prefer for-each when you don't need the index, keep the update only in the header, and iterate a snapshot or use an Iterator's remove when modifying during traversal.

code

java · 12 lines
java
// Safe removal during iteration:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (shouldDrop(it.next())) {
        it.remove(); // safe; for-each removal would throw
    }
}

// Hoist a recomputed bound:
for (int i = 0, n = list.size(); i < n; i++) {
    use(list.get(i));
}

go deeper

for a junior

Knows the counter is loop-scoped and that < vs <= causes off-by-one.

for a middle

Avoids mutating the counter in the body and recomputed bounds; uses for-each when no index is needed.

for a senior

Handles safe modification during iteration (Iterator.remove, backward loop), overflow, and empty-body traps with confidence.

for a principal

Codifies iteration-safety conventions and reviews for subtle liveness/overflow/CME defects across the codebase.

## Scope of the loop counter A variable declared in the **initialization clause** of a `for` loop — `for (int i = 0; ...)` — has a **scope limited to the loop** (its header and body). Once the loop ends, `i` **no longer exists**; referencing it afterward is a **compile error**. This is a feature: it prevents the counter from leaking into surrounding code and being accidentally reused or shadowed. If you *need* the final value after the loop (e.g., the index where you stopped), declare the variable **before** the loop instead. ## Off-by-one errors (`<` vs `<=`) An **off-by-one error** is running the loop one iteration too many or too few. The most common source is the comparison operator: - For array/list indices `0 .. length-1`, use **`i < length`**. There are exactly `length` valid indices. - Using **`i <= length`** runs one extra time and accesses index `length`, which is **out of bounds** -> `ArrayIndexOutOfBoundsException` / `IndexOutOfBoundsException`. - Counting **1 through n inclusive** uses `for (int i = 1; i <= n; i++)`. Always ask: *is the bound inclusive or exclusive?* and pick the operator accordingly. ## Don't mutate the counter in the body The update clause already advances the counter. If the **body also changes** `i` (e.g. an extra `i++` or `i += 2` inside), the loop's iteration count becomes hard to reason about and is a frequent bug. Keep all counter changes **in the header** unless you have a deliberate, documented reason. ## Modifying a collection while looping by index Iterating a list by index while **adding/removing** elements changes its `size()` mid-loop: - Removing element at `i` shifts later elements left; if you still do `i++`, you **skip** the next element. - The bound `i < list.size()` is **re-evaluated each pass**, so a shrinking/growing size can cause skips or overruns. Safe approaches: iterate a **copy/snapshot**, loop **backwards** when removing (`for (int i = n-1; i >= 0; i--)`), or use an **`Iterator`** and call `iterator.remove()` (the only safe in-place removal during iteration; otherwise you risk `ConcurrentModificationException` with for-each). ## Recomputed bounds `for (int i = 0; i < expensive(); i++)` re-runs `expensive()` **every iteration**. If it is costly or its value changes, hoist it: `for (int i = 0, n = expensive(); i < n; i++)`. Beware: if the underlying data legitimately changes size and you want to react, recomputation may be intended — be explicit either way. ## Integer overflow -> accidental infinite loop If the bound is near `Integer.MAX_VALUE`, `i++` can **overflow** to a large negative number, making `i < bound` perpetually true -> an **infinite loop**. Use a wider type (`long`) or a guard when iterating to the top of the int range. ## The stray-semicolon empty body ```java for (int i = 0; i < n; i++); // <-- note the ';' doWork(); // runs ONCE, not n times ``` A semicolon right after the header makes the loop body **empty**; the indented statement runs once afterward. This is a silent logic bug; many linters flag it. ## Prefer the right tool When you only traverse elements, **for-each** (`for (T x : coll)`) eliminates index bugs entirely. Use an index `for` only when you need the **position** or are deliberately mutating by index. ## Terms defined - **Scope**: the region where a name is visible/usable. - **Off-by-one**: a loop boundary error of one iteration. - **IndexOutOfBoundsException**: thrown when accessing an index outside `0 .. length-1`. - **ConcurrentModificationException**: thrown by many collections when structurally modified during for-each iteration. - **Integer overflow**: wraparound when an int exceeds its 32-bit range.

  • How do you safely remove elements from a list while iterating?
    Use an explicit Iterator and call iterator.remove(), iterate backwards by index, or build a new filtered list; do not remove inside a for-each (ConcurrentModificationException).
  • Why can a loop bounded near Integer.MAX_VALUE never terminate?
    i++ overflows past MAX_VALUE to a negative number, so i < bound stays true forever; use long or a guard.
  • What does for (int i = 0; i < n; i++); followed by a statement do?
    The trailing semicolon makes the loop body empty; it iterates doing nothing n times, then the following statement runs once.

saying these in an interview costs you the question

  • Using i <= length for array indices
  • Reading the counter after the loop assuming it persists
  • Removing from a list inside a for-each (CME) or by index with i++
  • Putting an extra i++ in the body and the header
  • Ignoring that i < list.size() is re-evaluated each pass

context