Command Palette

Search for a command to run...

PHASE 3Beginner ~28 min· topic 6 of 11

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 and if branches 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.

Main.javawhole filejava
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
}
Blocks set the boundariesdiagram
Rendering diagram…

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. 1

    Use a loop variable after the loop

    In "Block scope in action", add System.out.println(i); after the for loop. Read the error. Then move the declaration before the loop (int i; for (i = 1; ...)) and see it print 4.

  2. 2

    Make visit remember with a local

    In "Locals reset every call", try to make local count calls by moving local++ above the declaration or removing = 0. Neither works. Convince yourself why only a field can carry a value across calls.

  3. 3

    Trigger definite assignment

    Write int x; if (args.length > 0) { x = 1; } System.out.println(x);. Read the error, then fix it with an else branch that also assigns x.

Code & diagrams

Block scope in action New tab
Sign in to run this example in your browser.

Expected output

i = 1, square = 1
i = 2, square = 4
i = 3, square = 9
inner temp = 42
outer temp = 7, total = 14
Locals reset every call; static fields remember New tab
Sign in to run this example in your browser.

Expected output

local = 1, calls = 1
local = 1, calls = 2
local = 1, calls = 3
A local shadowing a static field New tab
Sign in to run this example in your browser.

Expected output

plain count = 5
Main.count  = 10
plain count = 10
defaults = 0, 0

Break 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.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            System.out.println(i);
        }
        System.out.println(i);
    }
}
terminal
$ javac Main.java
── what you'll see ──
Main.java:6: error: cannot find symbol
System.out.println(i);
^
symbol: variable i
location: class Main
1 error

Break #2

Redeclare a local in a nested block

Declare int x in a method and again inside an if block.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        int x = 1;
        if (x > 0) {
            int x = 2;
        }
    }
}
terminal
$ javac Main.java
── what you'll see ──
Main.java:5: error: variable x is already defined in method main(String[])
int x = 2;
^
1 error

Break #3

Read a local before assigning it

Declare int total; and print it immediately.

Main.javawhole filejava
public class Main {
    public static void main(String[] args) {
        int total;
        System.out.println(total);
    }
}
terminal
$ javac Main.java
── what you'll see ──
Main.java:4: error: variable total might not have been initialized
System.out.println(total);
^
1 error

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, so max_locals in 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 final or effectively final (Topic 10.1): if the copy and the original could diverge, the code would be confusing.

  • ▸

    Locals declared in a switch statement's case groups share one block scope for the whole switch (traditional case 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. 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 reports cannot find symbol.

  2. 2

    A for loop's variable (for (int i = 0; ...)) is scoped to the loop. Parameters are scoped to the whole method body. A variable declared inside an if, else or loop body is gone after that body's closing brace.

  3. 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. 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. 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 at 0, false or null.

  6. 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, where this.name = name is used (Topic 4.4).

Explain it without notes

01

What is the difference between scope and lifetime? Give an example where they differ.

02

Why does a local variable used as a counter start again from zero on every call?

03

Why does Java refuse to compile a read of an unassigned local, but allow reading an unassigned field?

04

What is shadowing, and how do you reach a shadowed static field?

Practice

01

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.

02

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);

03

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 defined and might not have been initialized errors.

  • 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.