Topic 3.6
Scope and Lifetime of Variables
In one line
A variable's scope is the part of the code where its name can be used; its lifetime is how long its storage exists while the program runs. Locals live in a block and die with their method call, fields live as long as their object, and static fields live as long as their class.
Think of it like this
A sticky note on a desk in a meeting room. While the meeting is on, everyone in that room can read the note (its scope is the room). When the meeting ends, the cleaners throw it away (its lifetime ends). People in the next room never saw it, and tomorrow's meeting in the same room starts with a clean desk.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Scope
- The part of the source code where a variable's name can be used.
- Lifetime
- The period during a run when a variable's storage exists in memory.
- Block
- A group of statements between
{and}. Method bodies, loop bodies andifbranches are blocks. - Local variable
- A variable declared inside a method (including parameters). It exists only during that method call.
- Field
- A variable declared directly inside a class, outside any method. Each object has its own copy, unless it's static.
- Static field
- A field marked
static: one copy shared by the whole class, living as long as the class is loaded. - Shadowing
- When a local variable has the same name as a field, so the plain name refers to the local and hides the field.
- Definite assignment
- The compiler rule that a local variable must surely have been given a value before it's read.
Step by step
01Blocks set the boundaries
Every { ... } creates a block, and blocks nest. A variable is visible from where it's declared to the closing brace of the block that contains it, including all nested blocks inside.
Inner blocks can see outer variables; outer code can't see inner ones. Think of one-way glass: you can look out of the small room into the big one, never in.
public static void main(String[] args) { // block A starts
int total = 0; // visible to the end of A
for (int i = 1; i <= 3; i++) { // block B: i and the body
int square = i * i; // visible to the end of B
total += square; // fine: total is from A
}
System.out.println(total); // fine
System.out.println(square); // error: cannot find symbol
}02Why loop variables vanish
for (int i = 0; i < 3; i++) declares i as part of the loop, so it lives only inside the loop. Afterwards the name i is free, and you can write a second loop with its own i.
If you need the final value after the loop, declare the variable before it: int i; for (i = 0; i < 3; i++) {} and i is 3 afterwards.
A variable declared inside the loop body is created fresh on every iteration. Its value doesn't carry over to the next pass.
03No duplicate locals, but locals can shadow fields
Inside one method, two locals with the same name can't be in scope at the same time, even in a nested block. C and C++ allow that; Java deliberately doesn't, because it hides bugs.
Once a block ends, its names are free, so a later block can reuse a name.
A local *can* share a name with a field. Inside that method the plain name means the local; to reach a static field use ClassName.field, and to reach an instance field use this.field (Topic 4.4).
04Locals must be assigned before use
Fields get default values automatically. Locals don't: reading one before giving it a value is a compile error. The compiler follows every path, so int x; if (ok) x = 1; System.out.println(x); fails, because when ok is false x has no value.
This rule (JLS chapter 16, *Definite Assignment*) catches a whole class of bugs that in C would read leftover garbage from memory.
05Lifetime: each call gets fresh locals
When a method is called, the JVM creates a stack frame holding all its locals. When the method returns, the frame is discarded. Call it again, and you get a new frame with new locals.
So a local can't remember anything between calls. A value that must survive, like a count of calls, needs a longer lifetime: a static field (lives with the class) or an instance field (lives with an object).
Objects are different from the variables pointing at them. A method can create an array, store the reference in a local, and return it. The local dies with the frame, but the array lives on the heap as long as something still refers to it (Topic 3.11).
06Keep scope as small as possible
Declare each variable in the smallest block that needs it, right where it's first used. A variable that's visible for 200 lines can be changed by any of them; one that's visible for 5 lines is easy to reason about.
Small scopes also help the JVM: a local slot whose variable is out of scope can be reused for another local, and the JIT knows sooner that an object is no longer needed.
Try it yourself
- 1
Use a loop variable after the loop
In "Block scope in action", add
System.out.println(i);after theforloop. Read the error. Then move the declaration before the loop (int i; for (i = 1; ...)) and see it print4. - 2
Make visit remember with a local
In "Locals reset every call", try to make
localcount calls by movinglocal++above the declaration or removing= 0. Neither works. Convince yourself why only a field can carry a value across calls. - 3
Trigger definite assignment
Write
int x; if (args.length > 0) { x = 1; } System.out.println(x);. Read the error, then fix it with anelsebranch that also assignsx.
Code & diagrams
Expected output
i = 1, square = 1
i = 2, square = 4
i = 3, square = 9
inner temp = 42
outer temp = 7, total = 14Expected output
local = 1, calls = 1
local = 1, calls = 2
local = 1, calls = 3Expected output
plain count = 5
Main.count = 10
plain count = 10
defaults = 0, 0Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use a loop variable outside the loop
Print i after a for (int i = ...) loop.
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
System.out.println(i);
}
}Break #2
Redeclare a local in a nested block
Declare int x in a method and again inside an if block.
public class Main {
public static void main(String[] args) {
int x = 1;
if (x > 0) {
int x = 2;
}
}
}Break #3
Read a local before assigning it
Declare int total; and print it immediately.
public class Main {
public static void main(String[] args) {
int total;
System.out.println(total);
}
}Myth vs fact
Myth
Local variables start at 0 like fields.
Fact
Only fields and array elements get defaults. Reading an unassigned local is a compile error.
Myth
When a local variable goes out of scope, the object it pointed to is destroyed.
Fact
Only the reference disappears. The object lives on the heap until nothing refers to it, and then the garbage collector reclaims it at some later time.
Myth
Declaring variables at the top of a method is cleaner.
Fact
That was a C89 habit. In Java, declare at first use in the smallest block; it limits who can change the variable.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Scope is checked by
javac; at run time locals are just numbered slots in the frame. javac reuses slots: once a block ends, a later variable can take the same slot, somax_localsin the class file is often smaller than the number of declared locals. - ▸
Reachability, not scope, decides garbage collection. JIT-compiled code tracks which locals are still *used*, so an object can be collected while its variable is technically still in scope. This is why
Reference.reachabilityFence(obj)(Java 9) exists: to keep an object alive until a point when native resources depend on it. - ▸
Lambdas and anonymous classes may outlive the frame they were created in, so they capture a copy of local values. That's why captured locals must be
finalor effectively final (Topic 10.1): if the copy and the original could diverge, the code would be confusing. - ▸
Locals declared in a
switchstatement's case groups share one block scope for the whole switch (traditionalcase X:form), so the same name can't be declared in two cases without extra braces. Arrow-form cases (Topic 2.4) each have their own scope.
Remember this
- 1
Scope is a compile-time idea: the region of source code where a name is visible. A local variable's scope runs from its declaration to the end of the block (
{ ... }) it's declared in. Outside that block, the compiler reportscannot find symbol. - 2
A
forloop's variable (for (int i = 0; ...)) is scoped to the loop. Parameters are scoped to the whole method body. A variable declared inside anif,elseor loop body is gone after that body's closing brace. - 3
Lifetime is a run-time idea: how long the storage exists. A local variable lives in its method call's stack frame and disappears when the call returns. Every call gets fresh locals, which is why a local counter starts again at zero each time.
- 4
Fields belong to an object and live as long as the object does (Topic 4.2). Static fields belong to the class and live as long as the class is loaded, typically the whole program run. Use them when a value must survive between calls.
- 5
Locals have no default value: the compiler refuses to read one before it's definitely assigned (
variable x might not have been initialized). Fields and array elements, in contrast, start at0,falseornull. - 6
Java forbids declaring a local with the same name as another local that's still in scope (
variable x is already defined). A local may have the same name as a field, which shadows the field; that's common in constructors and setters, wherethis.name = nameis used (Topic 4.4).
Explain it without notes
What is the difference between scope and lifetime? Give an example where they differ.
Why does a local variable used as a counter start again from zero on every call?
Why does Java refuse to compile a read of an unassigned local, but allow reading an unassigned field?
What is shadowing, and how do you reach a shadowed static field?
Practice
Write a program with a static field nextId starting at 1 and a method static int newId() that returns the current id and increases it. Call it three times and print the ids.
Fix this so it compiles and prints the largest value: int max; for (int v : new int[] {3, 8, 5}) { if (v > max) max = v; } System.out.println(max);
Write a method that uses two separate for loops, both with a variable named i, to print 0 1 2 and then 2 1 0 on two lines.
Trade-offs
- ↔
Static fields make data outlive calls, but they're shared by everything in the program (and every thread), which makes code harder to test and reason about. Prefer passing values as parameters and returning results.
- ↔
Very small scopes add a few extra declarations, but each variable then has an obvious purpose and can't be changed by unrelated code.
Done when you can
Done when you can point to the exact region where any variable is in scope.
Done when you can explain scope (compile time) vs lifetime (run time).
Done when you can fix
cannot find symbol,already definedandmight not have been initializederrors.Done when you can choose between a local, an instance field and a static field based on how long the value must live.
Done when you can explain shadowing and reach a shadowed static field.