JAVA / LOOPS AND ARRAYS
Classic for loops and the loop variable scope
Write classic for loops with deliberate init, condition and update clauses, and know exactly where the loop variable lives and dies.
What you will learn
- Trace the fixed order: init once, then condition, body, update, condition again
- Declare the counter in init to scope it to the loop; hoist it to read it afterwards
- Predict the post-loop value: the first one that failed the condition, not the last used
- Drive two cursors with comma-separated declarations and updates in one header
Understanding Classic for loops and the loop variable scope
A classic for header holds three independent slots separated by semicolons: a statement that runs once before anything else, a boolean expression tested before every pass, and an expression list that runs after every pass. Java always evaluates them in that order, which means the condition is evaluated exactly one time more than the body runs, since the loop can only stop by failing a test. The point of packing the bookkeeping into the header is that a reader sees the start, the stop and the step in one line instead of hunting for a counter update somewhere in the body.
A variable declared in the init clause is scoped to the for statement and nothing more: it is visible in the condition, in the update and in the body, and it stops existing at the closing brace. That is why the next loop in the same method can declare a variable with the same name without conflict, and why touching that name after the loop is a compile error rather than a puzzling runtime value. It also means you cannot redeclare the same name inside the body, because the body sits inside that scope. When you genuinely need the counter afterwards, declare it above the loop and only assign it in the init clause; the value you then read is the first one that failed the condition, so a loop bounded by i < 3 leaves 3 behind, not 2.
The counter is a single storage slot reused by every pass, and the update clause reassigns it. That makes it not effectively final, so a lambda or anonymous class inside the body cannot capture it, because a captured value must be fixed at the moment the closure is created. A variable declared inside the body is different: each pass creates a fresh one and assigns it once, so copying the counter into such a local gives every closure its own frozen value. Holding both pictures in mind, one slot for the whole loop versus one slot per pass, explains most surprises about for loop variables.
public class ForScope {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println("body sees i = " + i);
}
// i does not exist here; the name died with the for statement
int last = -1;
for (int i = 0; i < 3; i++) {
last = i;
}
System.out.println("last value the body saw: " + last);
int j;
for (j = 0; j < 3; j++) {
// j was declared before the loop, so it outlives the loop
}
System.out.println("j after the loop: " + j);
}
}A for header is three clauses with a fixed evaluation order, and a counter declared there lives exactly as long as the for statement itself.
Worked examples
Two cursors in one header
Shows comma-separated declarations and updates, and that the names are free again once the loop ends.
public class TwoIndices {
public static void main(String[] args) {
int[] data = {4, 8, 15, 16};
for (int left = 0, right = data.length - 1; left < right; left++, right--) {
System.out.println(data[left] + " pairs with " + data[right]);
}
for (int left = 0; left < 2; left++) {
System.out.println("left is a brand new variable: " + left);
}
}
}Example explained
Line 1The init clause declares left and right in one declaration, so both must have the same type int.
Line 2The update clause left++, right-- is a list of expressions; both run after each pass, before the next condition test.
Line 3The condition left < right fails when the cursors meet, so the four-element array produces two pairs.
Line 4The second loop redeclares left legally, because the first left ceased to exist at the first loop's closing brace.
Capturing the counter in a lambda
Demonstrates why the counter itself cannot be captured and how a body-local copy fixes it.
import java.util.ArrayList;
import java.util.List;
public class CaptureLoopVariable {
public static void main(String[] args) {
List<Runnable> jobs = new ArrayList<>();
for (int i = 0; i < 3; i++) {
int snapshot = i;
jobs.add(() -> System.out.println("job " + snapshot));
}
for (int k = 0; k < jobs.size(); k++) {
jobs.get(k).run();
}
}
}Example explained
Line 1Writing () -> System.out.println(i) instead would not compile, because i++ reassigns i and a captured local must be final or effectively final.
Line 2int snapshot = i; creates a new variable on every pass and assigns it once, which is exactly what capture requires.
Line 3Each Runnable keeps its own snapshot, so the jobs print 0, 1 and 2; Java rejects the shared-counter alternative outright instead of silently giving all three the same value.
Line 4The second loop counts with its own k, since i is out of scope by then and irrelevant to running the jobs.
Watching the clauses fire
Puts method calls in each clause so the evaluation order and the extra condition check become visible.
public class ForOrder {
public static void main(String[] args) {
for (int i = start(); check(i); i = next(i)) {
System.out.println(" body with i = " + i);
}
}
static int start() {
System.out.println("init runs once");
return 1;
}
static boolean check(int i) {
System.out.println("condition with i = " + i);
return i < 3;
}
static int next(int i) {
System.out.println("update from i = " + i);
return i + 1;
}
}Example explained
Line 1start() prints once, proving the init clause is not part of the repeating cycle.
Line 2Every body line is preceded by a condition line, because the test always guards the pass that follows it.
Line 3The update clause need not be an increment; i = next(i) is any expression that assigns the counter.
Line 4The final line shows the condition being evaluated a third time and failing, which is why the body ran twice but check ran three times.
Important notes
The init clause allows a single type declaration: for (int i = 0, double d = 0.0; ...) does not compile, so make both the same type or move one above the loop.
All three clauses may be left empty. for (;;) is legal and never fails a test on its own, and an empty update means nothing advances unless the body does it.
Common mistakes
Declaring the counter in the init clause and then using it after the loop: compilation fails with cannot find symbol, because its scope ended at the loop's closing brace.
Putting a semicolon straight after the header, as in for (int i = 0; i < 3; i++); { work(); } — the empty statement becomes the body, the loop spins three times doing nothing, and the block below runs exactly once.
Incrementing the counter in the update clause and again at the end of the body: it advances twice per pass, so the loop quietly visits only every second index and ends in half the passes.
Try it yourself
Change, predict, then run
In a browser editor, declare int best above a classic for loop over int[] temps = {12, 19, 7, 23} and use the loop to record the index of the largest value, then print best after the loop. Add a line that also prints the loop counter after the loop and read the compiler error it produces.
Open the Java workspaceCheck your understanding
Given int i; for (i = 0; i < 4; i += 2) { } System.out.println(i); what is printed?
- 2
- 3
- 4
- Nothing, because i is out of scope after the loop
Show answer
The loop ends only after the update produces a value the condition rejects: i becomes 0, 2, then 4, and the test on 4 fails, so 4 is printed. 2 is tempting because it is the last value the body saw, but the update always runs before the failing test. The last option would be right only if i had been declared inside the init clause, where its scope ends with the loop.