Java Scope
Scope is the region of a program where a name can be used. A variable declared in one place is visible in some parts of the code and invisible in others. Java's scope rules are strict and checked at compile time, which keeps variables from being used before they exist, after they are gone, or by code that has no business touching them.
Block Scope
In Java, scope is defined by blocks: the code between a pair of braces { }. A variable declared inside a block is visible from its declaration to the closing brace of that block, including any nested blocks.
public static void main(String[] args) {
int a = 1; // a: visible until the end of main
{
int b = 2; // b: visible until the end of this block
System.out.println(a + b); // both visible: 3
}
System.out.println(a); // 1
System.out.println(b); // compile error: cannot find symbol b
}
Two rules follow directly:
- A variable cannot be used before its declaration, even later in the same block.
- A variable cannot be used after its block closes.
System.out.println(total); // compile error: total not yet declared
int total = 10;
Method Scope
Local variables and parameters belong to one method call. They are invisible to other methods, even ones in the same class:
static void first() {
int count = 5;
}
static void second() {
System.out.println(count); // compile error: count belongs to first()
}
Each call also gets its own fresh copies. A local variable does not remember its value between calls:
static void visit() {
int visits = 0;
visits++;
System.out.println(visits);
}
visit(); // 1
visit(); // 1, not 2
To share data between methods, pass it as a parameter, return it, or store it in a field.
Scope in Control Structures
The body of an if, else, for, while, switch, or try is a block, so variables declared there are local to it:
if (isMember) {
double discount = 0.1;
}
System.out.println(discount); // compile error
Declare the variable before the block when the value is needed afterward:
double discount = 0;
if (isMember) {
discount = 0.1;
}
System.out.println(discount); // works
Loop variables
A variable declared in a for header is scoped to the loop:
for (int i = 0; i < 3; i++) {
System.out.println(i);
}
// i does not exist here
for (int i = 10; i < 13; i++) { // reusing the name is fine
System.out.println(i);
}
A variable declared inside a loop body is created fresh on every iteration:
for (int i = 0; i < 3; i++) {
int square = i * i; // new variable each pass
System.out.println(square);
}
Placing an accumulator inside the loop by mistake resets it every time:
for (int price : prices) {
int total = 0; // reset on every iteration
total += price;
}
// total does not even exist here
The accumulator must be declared before the loop.
catch parameters and try-with-resources
The exception variable in a catch exists only in that catch block, and resources declared in a try-with-resources header exist only in the try block:
try (Scanner sc = new Scanner(file)) {
process(sc);
} catch (IOException e) {
System.out.println(e.getMessage());
}
// neither sc nor e exists here
No Redeclaration in Nested Blocks
Java does not allow a local variable to be redeclared in a nested block while the outer one is still in scope:
int count = 0;
if (true) {
int count = 5; // compile error: variable count is already defined
}
Many languages permit this kind of local shadowing; Java forbids it because two local variables with the same name in overlapping scopes are almost always a mistake.
Sibling blocks that do not overlap may reuse names:
if (x > 0) {
String msg = "positive";
System.out.println(msg);
} else {
String msg = "not positive"; // separate scope: allowed
System.out.println(msg);
}
Class Scope: Fields
Variables declared in the class body, outside any method, are fields. They are visible throughout the class, in every method, regardless of where in the class they are declared:
public class Thermostat {
private double target = 22.0; // field: visible in all methods
void raise() {
target += 0.5;
}
void report() {
System.out.println("Target: " + target);
}
}
Fields live as long as their object (instance fields) or the loaded class (static fields), unlike locals, which disappear when the method returns. This is why a counter that must persist between calls is a field, not a local:
public class PageView {
private int views = 0;
void visit() {
views++;
System.out.println(views);
}
}
PageView page = new PageView();
page.visit(); // 1
page.visit(); // 2
Access modifiers (private, public, protected, package-private) control whether a field is visible from other classes. That is a separate concept from scope and is covered in the Access Modifiers lesson.
Shadowing Fields
A local variable or parameter may have the same name as a field. Within the local's scope, the simple name refers to the local, hiding the field. this.name reaches the field:
public class Product {
private String name;
private double price;
public Product(String name, double price) {
this.name = name; // field = parameter
this.price = price;
}
public void rename(String name) {
name = name.strip(); // modifies the parameter only
this.name = name; // updates the field
}
}
Without this., name = name; in the constructor would assign the parameter to itself and leave the field null. The compiler accepts it, which makes this one of the more confusing beginner bugs. Using this. for fields in constructors and setters is the standard convention.
Static Context and Scope
A static method has no object, so instance fields are not available in it by simple name:
public class App {
int counter = 0;
public static void main(String[] args) {
counter++; // compile error: non-static variable counter cannot be referenced from a static context
}
}
The field is in scope (it is declared in the class), but there is no instance to read it from. Either create an object (new App().counter++) or make the field static.
Lambdas and Effectively Final Variables
A lambda or anonymous class may read local variables from the enclosing method only if those variables are effectively final: never reassigned after initialization.
int threshold = 50;
List<Integer> high = scores.stream()
.filter(s -> s > threshold) // allowed: threshold is never changed
.toList();
int count = 0;
scores.forEach(s -> count++); // compile error: count is modified
The lambda may run later or on another thread, after the method's frame is gone, so Java captures a copy of the value. Allowing modification would make the copy and the original diverge. Lambdas have their own section.
Scope Summary
| Declared in | Visible | Lifetime |
|---|---|---|
A block { } | From declaration to end of the block | Until the block exits |
A for header | The loop header and body | Until the loop ends |
| A method parameter | The entire method body | One method call |
A catch clause | That catch block | While handling the exception |
| A class body (instance field) | All instance methods and constructors of the class | As long as the object exists |
A class body (static field) | All methods of the class | As long as the class is loaded |
Keeping Scope Narrow
Declaring each variable in the smallest scope that works, as close as possible to its first use, has concrete benefits:
- A reader sees the declaration and the use together.
- The variable cannot be accidentally reused or modified by unrelated code later in the method.
- The garbage collector can reclaim objects sooner, since references in finished scopes are gone.
// Wide scope: line and parts exist for the whole method
String line;
String[] parts;
for (int i = 0; i < rows.size(); i++) {
line = rows.get(i);
parts = line.split(",");
process(parts);
}
// Narrow scope: each exists only where it is used
for (String line : rows) {
String[] parts = line.split(",");
process(parts);
}
Practical Example: Tracking Scope in a Small Program
public class LibraryStats {
private static int totalLoans = 0; // class scope: shared state
static int countOverdue(int[] daysBorrowed, int limit) { // parameters: method scope
int overdue = 0; // method scope
for (int days : daysBorrowed) { // loop scope
if (days > limit) {
int lateBy = days - limit; // if-block scope
System.out.println("Late by " + lateBy + " days");
overdue++;
}
totalLoans++;
}
return overdue;
}
public static void main(String[] args) {
int[] branchA = {5, 16, 21, 9};
int[] branchB = {30, 2};
int overdueA = countOverdue(branchA, 14);
int overdueB = countOverdue(branchB, 14);
System.out.println("Overdue: " + (overdueA + overdueB));
System.out.println("Loans processed: " + totalLoans);
}
}
Output:
Late by 2 days
Late by 7 days
Late by 16 days
Overdue: 3
Loans processed: 6
lateBy exists only within the if that needs it. days exists only within the loop. overdue starts at 0 on each call, so each branch is counted separately and returned. totalLoans is a static field, so it survives between calls and accumulates across both branches. Each variable's position in the code decides exactly how long it lives and who can see it.