JAVA / METHODS
Variable scope, shadowing and lifetime
Read any Java method and say exactly where each variable is usable, how long its storage lives, and which declaration a repeated name resolves to.
What you will learn
- Name the exact brace pair that bounds a local variable's scope
- Use this.field or ClassName.field to reach a field a parameter shadows
- Declare accumulators before a loop, since body locals are recreated each pass
- Fix "might not have been initialized" by assigning on every reachable branch
Understanding Variable scope, shadowing and lifetime
Scope in Java is a region of source text, not a moment in time. A local variable's scope starts at its declaration and ends at the closing brace of the block that contains it, so a name declared in the third line of an if block is unusable on the second line of that block and unusable after the block closes. The header of a for loop counts as part of the loop's own block, which is why the i in for (int i = 0; ...) disappears the moment the loop ends, and why you may then declare a completely unrelated i in the same method.
Lifetime is the runtime companion to scope. Every call to a method gets its own fresh set of locals, and every pass through a loop body creates that body's locals again from nothing, which is why nothing accumulates across iterations unless the variable is declared outside the loop. Fields live as long as the object that holds them and static fields as long as the class stays loaded. Locals also differ from fields in one hard way: fields are zeroed to 0, false or null for you, while a local must be provably assigned before you read it or the compiler refuses to build the class.
Shadowing happens when two declarations of the same name are both technically visible and the innermost one wins. Java allows this only between a local or parameter and a field, and it gives you an escape hatch, this.name for an instance field and ClassName.name for a static one. Two locals is a different story: the compiler rejects a second int x inside a nested block while the outer x is still in scope, which removes an entire class of bug that C programmers know well. The everyday use of legal shadowing is a constructor or setter parameter deliberately named after the field it fills.
placeholder
public class ScopeDemo {
static int counter = 0; // lives as long as the class is loaded
static void tick(int counter) { // parameter shadows the field
counter++; // touches the parameter only
ScopeDemo.counter++; // qualified name reaches the field
System.out.println("parameter=" + counter + " field=" + ScopeDemo.counter);
}
public static void main(String[] args) {
tick(10);
tick(10);
for (int i = 0; i < 3; i++) {
int seen = i * i; // created fresh on every iteration
System.out.println("i=" + i + " seen=" + seen);
}
// i and seen no longer exist here
int i = 99; // legal: the loop's i is out of scope
System.out.println("outer i=" + i + " field counter=" + counter);
}
}A name is usable only inside the braces where it was declared, and when a parameter and a field share a name the parameter wins unless you qualify the field with this. or the class name.
Worked examples
A forgotten this. loses the value
Shows what an unqualified assignment does when a parameter shadows the field of the same name.
public class Account {
private String owner;
private int balance;
Account(String owner, int balance) {
this.owner = owner; // field on the left, parameter on the right
balance = balance; // both sides are the parameter
}
@Override
public String toString() {
return owner + ":" + balance;
}
public static void main(String[] args) {
System.out.println(new Account("Ada", 500));
}
}Example explained
Line 1this.owner = owner reads the parameter and writes the field, because this. steps past the local scope.
Line 2balance = balance resolves both names to the parameter, so the field keeps its default 0 and the 500 is thrown away.
Line 3Inside toString no parameter is in scope, so the bare names owner and balance refer to the fields.
Line 4javac reports nothing here, so the only evidence of the bug is the 0 in the printed line.
Blocks, reuse and definite assignment
Shows a variable declared wide enough to outlive an if/else, and names reused legally once the earlier block has closed.
public class BlockScope {
public static void main(String[] args) {
int hour = 14;
String label; // declared out here so it survives the if/else
if (hour < 12) {
String part = "morning";
label = part;
} else {
String part = "afternoon"; // legal: the other part is out of scope
label = part;
}
// System.out.println(part); // would not compile: part is gone
System.out.println(label);
{
int temp = hour * 2;
System.out.println("temp=" + temp);
}
int temp = 7; // legal: the block's temp no longer exists
System.out.println("temp=" + temp);
}
}Example explained
Line 1String label; sits above the if, so its scope covers both arms and everything after them.
Line 2Each arm declares its own part; the two scopes never overlap, so the repeated name is fine.
Line 3Printing label compiles only because every path through the if/else assigns it; delete the else and you get "variable label might not have been initialized".
Line 4The bare block gives temp a three-line lifetime, after which int temp = 7 declares an unrelated variable that happens to share the name.
Important notes
Java refuses to let a local or parameter shadow another local that is still in scope, so a duplicate int x in a nested block is a compile error; only fields can be shadowed this way, and lambda parameters cannot shadow enclosing locals either.
Scope and lifetime are different: an object created inside a block outlives the block if you return it or store it in a field, since only the variable that named it disappears at the brace.
Common mistakes
Writing balance = balance; in a constructor or setter instead of this.balance = balance;: javac accepts it, both sides mean the parameter, and the field silently stays 0 or null until a later NullPointerException or a zero on screen.
Declaring String label inside an if block and printing it after the closing brace: the build fails with "cannot find symbol", and the fix is to move the declaration above the if rather than repeat it in the else.
Putting int sum = 0; inside the loop body instead of before it: it compiles and runs, but sum is a new variable every iteration, so the reported total equals only the last element's contribution.
Try it yourself
Change, predict, then run
In a browser editor, write a Point class whose constructor parameters are named x and y, assign one field with this.x = x and the other with a plain y = y, and print new Point(3, 4). Then add the missing this. and compare the two outputs.
Open the Java workspaceCheck your understanding
Java lets you declare int x in both arms of an if/else, but rejects a second int x inside a nested block of a method that already declared x. What explains the difference?
- The two arms are disjoint regions of source, so neither x is in scope in the other, while a nested block lies inside the outer x's scope and Java forbids a local from shadowing another local.
- Each if/else arm gets its own stack frame, while a nested block reuses the frame of the enclosing method.
- The compiler checks for duplicate names once per method and treats a whole if/else statement as a single check.
- A variable declared in an if arm is implicitly final, and final variables may be redeclared freely.
Show answer
Scope is a region of source text, and the else arm is outside the then arm's region, so the name is free to be reused there; a nested block sits inside the outer declaration's region, and the language rule allows only fields to be shadowed, never another local or parameter. The stack frame answer is tempting but wrong: one method invocation gets exactly one frame, entering a block creates no new frame, and any reuse of storage slots is an invisible optimisation rather than the reason the code compiles.