Module 4 · Methods & Modularity ⏱ 18 min

Scope, Lifetime, and Shadowing

By the end of this lesson you will be able to:
  • Determine where a local variable is visible from its declaring block
  • Describe a variable's lifetime from declaration to the closing brace
  • Predict values under shadowing, and explain how this. reaches a hidden field

Imagine every variable you ever named lived in one giant shared space. The moment you reused i in two loops, or total in two methods, the second one would silently overwrite the first — and your program would behave differently depending on the order methods ran. Java prevents that chaos with scope: each variable exists only inside the region where it was declared, and is invisible everywhere else.

Scope is what lets ten methods each declare their own count without any of them colliding. It is also the rule behind the compiler's most frequent complaint — cannot find symbol — which is almost always a variable used outside the block where it was born.

flowchart TD
  M["method scope<br/>declares int total"] --> B1["inner if-block<br/>declares int bonus"]
  B1 -.->|"reads outer total"| M
  M -.->|"cannot see inner bonus"| B1
  style M fill:#3776ab,color:#fff
  style B1 fill:#b45309,color:#fff
Visibility flows outward: an inner block can read an outer variable, but an outer block cannot reach into an inner one.

Blocks define scope

A block is any stretch of code wrapped in { }. A method body is a block; so is the body of an if, a for, or a while. A variable declared inside a block is local to it — it can be read and changed by code within that block, but the moment execution passes the closing brace, the variable ceases to exist.

Visibility flows outward, not inward. Code in an inner block can see variables declared in any enclosing block, but an outer block cannot look into an inner one. That asymmetry is deliberate: inner blocks are always built on top of what the outer block has already established.

Main.java — a reassignment inside a block persists because it is the same variable; an inner declaration vanishes at the brace. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        int total = 10;
        if (total > 0) {
            total = total + 5;     // same variable: change persists
            int bonus = 2;         // visible only inside this block
            total = total + bonus;
        }
        // bonus is out of scope here; using it would not compile
        System.out.println(total);
    }
}

Lifetime: from declaration to the closing brace

A variable's lifetime is the window during which it actually holds storage. It begins at the declaration — not at the top of the block — and ends at the brace that closes the block containing that declaration. Use a variable even one line before its declaration and the compiler refuses, because it does not exist yet.

Each time a block is entered, its locals are created fresh; when the block exits, they are discarded. That is why a variable declared inside a loop is reset on every iteration, and why a method called twice starts with a clean slate each time — its parameters and locals are rebuilt from nothing on every call.

flowchart LR
  D["declaration<br/>int x = 5"] --> U["x is in use"]
  U --> E["closing brace }<br/>x is destroyed"]
  style D fill:#15803d,color:#fff
  style E fill:#b91c1c,color:#fff
A local lives from its declaration to the closing brace that encloses it.

Loop variables and parameters

A for loop's header is itself a scope: for (int i = 0; ...) declares an i that lives only for the loop's duration and is gone the instant it ends. Two loops in the same method may each declare their own i — they never conflict, because neither can see the other's. The same courtesy applies to method parameters: a parameter's scope is the whole method body, and parameters of different methods are entirely independent even when they share a name.

This is why you so often see i, j, and n reused everywhere in real code. Scope makes those names cheap — each use is quarantined to its own block.

Main.java — two sibling loops each declare their own i; neither sees the other. Real, compiling Java.
public class Main {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 1; i <= 3; i++) {
            sum = sum + i;
        }
        for (int i = 10; i <= 11; i++) {   // separate i, no conflict
            sum = sum + i;
        }
        System.out.println(sum);
    }
}

Shadowing: when names collide on purpose

A field declared on a class is visible throughout every method of that class. So what happens when a method declares a parameter or local with the same name as a field? Java does not error — it applies nearest wins: the bare name resolves to the closest declaration, which is the parameter or local, and the field is temporarily hidden, or shadowed.

This is usually deliberate in constructors and setters, where void setBalance(int balance) wants the parameter spelled exactly like the field. To reach the hidden field you must write this.balancethis means the current object, breaking the tie in the field's favour. Forgetting this. is the silent bug behind half of all 'why didn't my setter work?' questions.

flowchart LR
  F["field balance = 100"] -.->|"hidden by"| P["param balance = 0"]
  P --> A["bare balance = the param<br/>nearest wins"]
  A --> THIS["this.balance = the field"]
  style F fill:#6b7280,color:#fff
  style P fill:#b45309,color:#fff
  style THIS fill:#3776ab,color:#fff
A parameter that shares a field's name shadows it: the bare name means the parameter, while this. reaches the field.
Main.java — this.balance reaches the field while the bare balance means the parameter. Real, compiling Java.
public class Main {
    static class Account {
        int balance;

        Account(int balance) {
            this.balance = balance;   // this.balance = field; balance = param
        }

        void deposit(int balance) {
            this.balance = this.balance + balance;
        }
    }

    public static void main(String[] args) {
        Account a = new Account(100);
        a.deposit(50);
        System.out.println(a.balance);
    }
}
Exercise

Block scope. A variable is reassigned inside an if block. Predict the single line printed in main — remember the inner bonus is not visible outside, but total is the same variable throughout.

public class Main {
    public static void main(String[] args) {
        int total = 10;
        if (total > 0) {
            total = total + 5;
            int bonus = 2;
            total = total + bonus;
        }
        System.out.println(total);
    }
}
Exercise

Shadowing trap. fakeDeposit adds 50 to its parameter. Predict the single line — does the field change?

public class Main {
    static class Account {
        int balance;

        Account(int balance) {
            this.balance = balance;
        }

        void fakeDeposit(int balance) {
            balance = balance + 50;   // changes the parameter, not the field
        }
    }

    public static void main(String[] args) {
        Account a = new Account(100);
        a.fakeDeposit(0);
        System.out.println(a.balance);
    }
}
Exercise

Sibling scopes. Two loops each declare their own i. Predict the single line printed by main.

public class Main {
    public static void main(String[] args) {
        int sum = 0;
        for (int i = 1; i <= 3; i++) {
            sum = sum + i;
        }
        for (int i = 10; i <= 11; i++) {
            sum = sum + i;
        }
        System.out.println(sum);
    }
}
Exercise

A variable is declared inside the block of an if statement. Which statement about it is correct?

Exercise

A class has a field int count, and a method void bump(int count) { count = count + 1; }. After calling bump(5) on an object whose count was 0, what is the field's value?

Recap

  • A variable's scope is the block { } where it is declared; it is invisible outside that block.
  • Lifetime runs from the declaration to the block's closing brace; locals are rebuilt each time a block is entered.
  • Inner blocks can read outer variables; outer blocks cannot reach into inner ones.
  • A for loop's variable exists only for the loop, so sibling blocks may each reuse the same name without conflict.
  • When a parameter or local shares a field's name it shadows the field — bare names mean the nearest declaration; use this. to reach the field.

Next you will watch a method call itself, and the scope discipline you have just learned is exactly what keeps recursion from spinning forever.

Checkpoint quiz

What determines a local variable's lifetime in Java?

A parameter has the same name as a field. Inside the method body, a bare use of that name refers to…

Go deeper — technical resources