The scope of a variable is the region of source code in which its name means that variable. Outside that region the name is not merely empty — it does not exist, and javac rejects the program. Scope is decided entirely by where the declaration sits, and in Java that means: by the braces around it.
This article covers the four kinds of variable a Java program can declare, where each one is visible, when each one is created and destroyed, and the errors that appear when a read falls outside the region the compiler allows. Every output line and every error message below was produced by compiling and running the code on OpenJDK 21.0.6.
![]()
One rule does most of the work: a name lives from its declaration to the closing brace of the block that contains it.
Java has no global variable
Many tutorials, and many course outlines, contrast "local variables" with "global variables". Java has no such thing. There is no place to put a variable that is not inside a class, so there is no name that every part of a program can reach without qualification.
The nearest equivalent is a static field: a variable declared directly in a class body with the static keyword. One copy exists per class, it is reachable from any method of that class by its bare name, and from anywhere else as ClassName.field.
public class NoGlobal {
// the nearest thing Java has to a "global variable": a static field
static int requestCount = 0;
static void handle() {
requestCount++; // no object needed, no parameter passed
}
public static void main(String[] args) {
handle();
handle();
handle();
System.out.println("NoGlobal.requestCount = " + NoGlobal.requestCount);
System.out.println("Counter.total = " + Counter.total);
Counter.total = 42;
System.out.println("Counter.total = " + Counter.total);
}
}
class Counter {
static int total = 7; // reachable from another class by its name
}
NoGlobal.requestCount = 3
Counter.total = 7
Counter.total = 42
That is as global as Java gets, and it still has an owner. requestCount is not floating free; it belongs to NoGlobal. When the outline for this series says "local versus global", read it as "local versus static field", and the rest of the article is about the four places a name can actually live.
The four scopes a variable can have
Every variable in a Java program is one of four things, and its scope follows from which one it is.
| Kind | Declared where | Visible where | One copy per |
|---|---|---|---|
| Local variable | inside a method, constructor or block | from its declaration to the closing brace of that block | execution of the block |
| Method parameter | in the parameter list | the whole method body | call |
| Instance field | in the class body, no static | every instance method of the class | object |
static field | in the class body, with static | every method of the class | class |
The first two live on the call stack, the last two in the object or in the class. One program shows all four at once.
public class FourScopes {
static String appName = "scope-demo"; // static field: one copy, whole class
String user; // instance field: one copy per object
FourScopes(String user) {
this.user = user;
}
void report(int visits) { // visits: parameter, whole method body
String prefix = "[" + appName + "]"; // local: the method body
if (visits > 0) {
String note = visits + " visit(s)";// local: only this if block
System.out.println(prefix + " " + user + " -> " + note);
}
// note is gone here
System.out.println(prefix + " " + user + " reported");
}
public static void main(String[] args) {
FourScopes a = new FourScopes("ann");
FourScopes b = new FourScopes("bob");
a.report(2);
b.report(0);
System.out.println("appName = " + appName + ", a.user = " + a.user + ", b.user = " + b.user);
}
}
[scope-demo] ann -> 2 visit(s)
[scope-demo] ann reported
[scope-demo] bob reported
appName = scope-demo, a.user = ann, b.user = bob
appName is one value shared by everything. user is two values, one in each object. visits and prefix exist only while report is running, and note only while the if body is running. this.user appears in the constructor because a parameter named user is hiding the field there; that is the subject of the shadowing section below, and this itself gets a proper treatment in the article on classes and objects.
A field is visible from a static method only through an object
The class body is one scope, but a static method has no object, so an instance field has no storage to read from inside it.
public class StaticContext {
static int shared = 1;
int perObject = 2;
public static void main(String[] args) {
System.out.println("shared = " + shared);
System.out.println("perObject = " + perObject);
}
}
StaticContext.java:7: error: non-static variable perObject cannot be referenced from a static context
System.out.println("perObject = " + perObject);
^
1 error
The name perObject is in scope — the compiler found it, which is why the message is not cannot find symbol. What is missing is an object to read it from. Naming one fixes it:
StaticContextOk o = new StaticContextOk();
System.out.println("shared = " + shared);
System.out.println("perObject = " + o.perObject);
shared = 1
perObject = 2
This is the variable-shaped twin of non-static method cannot be referenced from a static context, covered in the article on methods.
Block scope: the closing brace is where the name ends
A block is any pair of braces: a method body, an if body, a loop body, or a bare { } written for no reason but grouping. A local declared inside a block ends at that block's closing brace, and afterwards the name does not exist.

public class Scopes {
static int classLevel = 1;
static void run() {
int methodLevel = 2;
if (methodLevel > 0) {
int blockLevel = 3;
use(classLevel, methodLevel, blockLevel);
}
use(classLevel, methodLevel);
use(blockLevel);
}
static void other() {
use(classLevel);
use(methodLevel);
}
static void use(int... values) { }
public static void main(String[] args) {
run();
other();
}
}
Scopes.java:11: error: cannot find symbol
use(blockLevel);
^
symbol: variable blockLevel
location: class Scopes
Scopes.java:16: error: cannot find symbol
use(methodLevel);
^
symbol: variable methodLevel
location: class Scopes
2 errors
Two errors, and both are the same error. Line 11 sits in run, one brace outside the if, so blockLevel is gone. Line 16 sits in other, an entirely different method, so methodLevel is gone. classLevel is read from three places without complaint, because a field's scope is the whole class body.
cannot find symbol is worth reading carefully: the compiler is not saying the variable is empty, uninitialized or out of range. It is saying there is no such name here at all.
The for header counter
The most common encounter with block scope is a loop counter. A variable declared in the initializer of a for header belongs to the loop, header and body together, and dies with it.
public class BlockScope {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println("i = " + i);
}
System.out.println("after the loop, i = " + i);
}
}
BlockScope.java:6: error: cannot find symbol
System.out.println("after the loop, i = " + i);
^
symbol: variable i
location: class BlockScope
1 error
If you need the counter afterwards — to know where a search stopped, say — declare it before the loop and leave the initializer empty:
int i = 0;
for (; i < 3; i++) {
System.out.println("i = " + i);
}
System.out.println("after the loop, i = " + i);
i = 0
i = 1
i = 2
after the loop, i = 3
Most of the time you do not need it, and the version that scopes i to the loop is the better one precisely because the name cannot leak.
A variable declared inside an if
The same rule, with a different brace. An if body is a block.
public class IfScope {
public static void main(String[] args) {
int age = 20;
if (age >= 18) {
String status = "adult";
}
System.out.println(status);
}
}
IfScope.java:7: error: cannot find symbol
System.out.println(status);
^
symbol: variable status
location: class IfScope
1 error
The fix is to split declaration from assignment: declare status in the scope that has to read it, and assign inside the branch.
int age = 20;
String status = "minor"; // declared in the scope that needs to read it
if (age >= 18) {
status = "adult"; // assignment, not declaration
}
System.out.println(status);
adult
A bare block behaves identically, which is the clearest demonstration that braces alone are what matter:
{
int temp = 5;
System.out.println("inside the block, temp = " + temp);
}
System.out.println("outside the block, temp = " + temp);
BareBlock.java:7: error: cannot find symbol
System.out.println("outside the block, temp = " + temp);
^
symbol: variable temp
location: class BareBlock
1 error
Other places a scope opens
Three more constructs declare a variable whose scope is the block they head. All three behave exactly like the for counter. The excerpt below sits in a class that imports java.util.Scanner.
int[] data = {1, 2, 3};
for (int n : data) { // n: the enhanced-for body
System.out.println("n = " + n);
}
try (Scanner sc = new Scanner("42")) { // sc: the try block
System.out.println("read " + sc.nextInt());
}
try {
throw new IllegalStateException("boom");
} catch (RuntimeException e) { // e: the catch block
System.out.println("caught " + e.getMessage());
}
n = 1
n = 2
n = 3
read 42
caught boom
Read n after the enhanced for and the message is the one you already know:
OtherScopesBad.java:6: error: cannot find symbol
System.out.println(n);
^
symbol: variable n
location: class OtherScopesBad
1 error
Declaration order: locals must come first, fields need not
For a local variable, the scope starts at the declaration, not at the opening brace. A read on an earlier line is outside it.
public class LocalOrder {
public static void main(String[] args) {
System.out.println("total = " + total);
int total = 10;
}
}
LocalOrder.java:3: error: cannot find symbol
System.out.println("total = " + total);
^
symbol: variable total
location: class LocalOrder
1 error
A field is different. Its scope is the whole class body, so a method may use a field declared far below it, and the file compiles and runs:
public class FieldOrder {
// show() reads a field that is declared 4 lines BELOW it
static void show() {
System.out.println("greeting = " + greeting);
}
static String greeting = "hello";
public static void main(String[] args) {
show();
}
}
greeting = hello
This is why the order of members in a class is a style decision rather than a compiler requirement, while the order of statements in a method is not.
There is one exception, and it is narrow: a field initializer may not read a field declared after it, because initializers run top to bottom.
public class FieldForward {
static int a = b + 1; // reads b before b is declared
static int b = 2;
public static void main(String[] args) {
System.out.println("a = " + a + ", b = " + b);
}
}
FieldForward.java:2: error: illegal forward reference
static int a = b + 1; // reads b before b is declared
^
1 error
The name is in scope — that is why the error is not cannot find symbol — but the value does not exist yet. Inside a method body the same reference is legal, because by the time any method runs, every initializer has already finished.
Definite assignment for locals, default values for fields
Scope decides whether a name exists. A separate rule decides whether it holds anything. The article on variables and data types covered it in full; the short version is that fields are default-initialized and locals are not.
public class FieldDefaultsOk {
static int staticCount; // no initializer
int instanceCount; // no initializer
String label; // no initializer
public static void main(String[] args) {
FieldDefaultsOk f = new FieldDefaultsOk();
System.out.println("staticCount = " + staticCount);
System.out.println("instanceCount = " + f.instanceCount);
System.out.println("label = " + f.label);
}
}
staticCount = 0
instanceCount = 0
label = null
Add one uninitialized local to the same main and the program stops compiling:
int local; // no initializer, and no default either
System.out.println("local = " + local);
FieldDefaults.java:13: error: variable local might not have been initialized
System.out.println("local = " + local);
^
1 error
The word to notice is might. The compiler is not tracking values; it is proving that every path reaching the read has already written the variable. One branch that forgets is enough to fail the proof:
public class BranchAssign {
public static void main(String[] args) {
int score = 55;
String grade; // declared, deliberately not initialized
if (score >= 60) {
grade = "pass";
}
System.out.println(grade);
}
}
BranchAssign.java:8: error: variable grade might not have been initialized
System.out.println(grade);
^
1 error
Adding an else that assigns grade makes the proof succeed and the program prints fail. This is also why splitting a declaration from its assignment, as in the if fix above, still needs every branch covered — or a starting value at the declaration.
Shadowing: when a local hides a field
Two variables may share a name if one is a field and the other is a local or a parameter. Inside the method, the nearer declaration wins, and the field becomes unreachable by its bare name. That is shadowing, and it is legal, deliberate and the source of one of the most common silent bugs in Java.

The broken setter
public class BrokenSetter {
String name = "unset";
void setName(String name) {
name = name; // assigns the parameter to itself
}
public static void main(String[] args) {
BrokenSetter b = new BrokenSetter();
System.out.println("before setName: b.name = " + b.name);
b.setName("ada");
System.out.println("after setName: b.name = " + b.name);
}
}
before setName: b.name = unset
after setName: b.name = unset
Both name tokens on that line resolve to the parameter, so the statement copies the parameter into itself and the field never moves. It is not a compile error, and it is not a warning either — javac -Xlint:all BrokenSetter.java prints nothing at all. The only evidence is the output.
this.name names the field explicitly, which is the fix:
void setName(String name) {
this.name = name; // field on the left, parameter on the right
}
before setName: f.name = unset
after setName: f.name = ada
this is the receiver of the current instance method, and it gets its own treatment in the article on classes and objects. For now, treat this.x as the way to say "the field, not the local".
Shadowing is not confined to parameters. Any local with a field's name does the same thing:
public class Shadowing {
int count = 100; // the field
void demo() {
System.out.println("field = " + count);
int count = 5; // a local that shadows the field
System.out.println("local = " + count);
System.out.println("field via this = " + this.count);
count++; // touches the local only
System.out.println("local after ++ = " + count);
System.out.println("field after ++ = " + this.count);
}
public static void main(String[] args) {
new Shadowing().demo();
}
}
field = 100
local = 5
field via this = 100
local after ++ = 6
field after ++ = 100
Note the first line: before the local's declaration, count still means the field. The name changes meaning halfway down the method. That is legal, and it is also a good argument for never writing it.
Shadowing a field on purpose has exactly one common use, the constructor or setter parameter named after the field, and there this. makes the intent explicit. Everywhere else, rename the local.
You cannot shadow a local with another local
The permission stops at fields. Two locals with the same name in the same method are rejected, whether the second one is in the same block or a nested one.
int x = 1;
int x = 2;
RedeclareSame.java:4: error: variable x is already defined in method main(String[])
int x = 2;
^
1 error
int x = 1;
if (x > 0) {
int x = 2; // a nested block cannot shadow an enclosing local
System.out.println(x);
}
RedeclareNested.java:5: error: variable x is already defined in method main(String[])
int x = 2; // a nested block cannot shadow an enclosing local
^
1 error
A parameter counts as a local for this rule:
static void f(int n) {
int n = 3; // a local cannot shadow a parameter either
System.out.println(n);
}
RedeclareParam.java:3: error: variable n is already defined in method f(int)
int n = 3; // a local cannot shadow a parameter either
^
1 error
Two sibling blocks may reuse a name, because the first one has already ended before the second begins:
public class SiblingBlocks {
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
System.out.println("first loop, i = " + i);
}
for (int i = 10; i < 12; i++) { // legal: the first i no longer exists
System.out.println("second loop, i = " + i);
}
if (args.length == 0) {
int n = 1;
System.out.println("then branch, n = " + n);
} else {
int n = 2; // legal: a sibling block, not a nested one
System.out.println("else branch, n = " + n);
}
}
}
first loop, i = 0
first loop, i = 1
second loop, i = 10
second loop, i = 11
then branch, n = 1
Run with no arguments, the else branch never executes, but it compiles: the two n declarations are in different blocks, and neither encloses the other.
Lifetime is not scope
Scope is a property of the source text: which lines may name the variable. Lifetime is a property of the running program: how long the storage exists. They are usually confused because for a local they coincide, and for a field they do not.

| Kind | Created | Destroyed | Copies alive at once |
|---|---|---|---|
| Local variable | when execution reaches its declaration | when the block ends, with the stack frame | one per active block |
| Method parameter | when the method is called | when the method returns | one per active call |
| Instance field | when the object is created | when the object becomes unreachable | one per object |
static field | when the class is loaded | when the class is unloaded, in practice at JVM exit | one |
Three counters in one class make the difference visible.
public class Lifetime {
static int staticCalls = 0; // lives from class load until the JVM exits
int instanceCalls = 0; // lives as long as this object is reachable
void tick() {
int localCalls = 0; // created and destroyed on every call
localCalls++;
instanceCalls++;
staticCalls++;
System.out.println("local " + localCalls
+ " | instance " + instanceCalls
+ " | static " + staticCalls);
}
public static void main(String[] args) {
Lifetime a = new Lifetime();
a.tick();
a.tick();
a.tick();
System.out.println("-- a second object --");
Lifetime b = new Lifetime();
b.tick();
b.tick();
}
}
local 1 | instance 1 | static 1
local 1 | instance 2 | static 2
local 1 | instance 3 | static 3
-- a second object --
local 1 | instance 1 | static 4
local 1 | instance 2 | static 5
Three shapes in one printout. localCalls never gets past 1, because each call gets a brand new slot initialized to 0. instanceCalls counts to 3 for a and then restarts at 1 for b, because each object carries its own copy. staticCalls climbs to 5, because there is one copy for the class and both objects increment it.
Recursion makes the local case sharper still: a method that calls itself has several frames alive at once, and each frame has its own copy of every local. The name is one name; the storage is one slot per frame.
Loop-body locals are re-created every iteration
A loop body is a block, so a variable declared inside it is created and destroyed on every pass. It cannot accumulate anything.
public class LoopLocal {
public static void main(String[] args) {
for (int i = 1; i <= 3; i++) {
int sum = 0; // re-created every iteration
sum += i;
System.out.println("inside i=" + i + " sum=" + sum);
}
int total = 0; // declared outside, so it survives the loop
for (int i = 1; i <= 3; i++) {
total += i;
System.out.println("outside i=" + i + " total=" + total);
}
}
}
inside i=1 sum=1
inside i=2 sum=2
inside i=3 sum=3
outside i=1 total=1
outside i=2 total=3
outside i=3 total=6
sum shows the value of one iteration, never the running total, because int sum = 0; runs again every pass. total accumulates because its declaration is outside the loop and executes once. Move a declaration across a brace and the meaning of the program changes.
Reading sum after the loop is the block-scope error again, so the mistake usually announces itself:
LoopLocalRead.java:7: error: cannot find symbol
System.out.println("sum = " + sum);
^
symbol: variable sum
location: class LoopLocalRead
1 error
The dangerous version is the one where a variable of that name already exists outside the loop, so the read compiles and quietly reports the last iteration instead of the total.
Nested blocks, and what lambdas add
An inner block can see everything the enclosing block declared before it, and can write to it. Nesting adds names; it never hides the outer ones, because a redeclaration is rejected outright.
public class NestedBlock {
public static void main(String[] args) {
int outer = 1;
{
int inner = 2;
outer += inner; // an inner block reads and writes the outer local
System.out.println("inner block: outer=" + outer + " inner=" + inner);
}
System.out.println("after block: outer=" + outer);
}
}
inner block: outer=3 inner=2
after block: outer=3
Ordinary blocks are that simple. Anonymous classes and lambdas are not: they can outlive the method that created them, so a local they capture must be final or effectively final — assigned once and never reassigned. Reassigning n after a lambda has captured it is rejected:
LambdaCapture.java:4: error: local variables referenced from a lambda expression must be final or effectively final
Runnable r = () -> System.out.println("n = " + n);
^
1 error
Capture rules belong with lambdas themselves and are covered in the advanced course. The reason is worth one sentence here: a lambda copies the captured local, so allowing reassignment would give two values that silently disagree.
Declare at the narrowest scope that works
The practical rule follows from everything above: declare each variable as close to its first use as possible, in the smallest block that contains every use. Not because narrow scope is tidy, but because it is what makes code movable.
// wide: every local lives to the end of the method, so nothing is separable
static void wide(int[] data) {
int sum = 0;
int max = data[0];
int i = 0;
for (i = 0; i < data.length; i++) {
sum += data[i];
if (data[i] > max) {
max = data[i];
}
}
System.out.println("wide: sum=" + sum + " max=" + max + " i=" + i);
}
Every name in wide is alive for the whole method, so a reader has to hold five of them at once, and extracting the maximum calculation into its own method means checking each one for a use further down. Narrow the scopes and the pieces come apart on their own:
static int sum(int[] data) {
int total = 0;
for (int n : data) {
total += n;
}
return total;
}
static int max(int[] data) {
int best = data[0];
for (int n : data) {
if (n > best) {
best = n;
}
}
return best;
}
wide: sum=21 max=9 i=4
narrow: sum=21 max=9
Same answers. The difference is that total and best cannot be read anywhere except the four lines that own them, so moving those lines is a mechanical operation rather than an investigation. A variable declared at the top of a long method is an unpaid debt: someone eventually has to work out how far its influence reaches.
Three habits follow from that. Declare inside the loop when the value is per-iteration and outside when it accumulates. Prefer the for header for a counter you do not need afterwards. And when you find yourself declaring a variable several lines before its first use, move the declaration down.
Common mistakes and the errors they produce
| Mistake | What happens | Message |
|---|---|---|
Reading a for counter after the loop | compile error | cannot find symbol |
Declaring inside an if and reading outside | compile error | cannot find symbol |
| Reading a local above its own declaration | compile error | cannot find symbol |
Reading a catch parameter or enhanced-for variable after its block | compile error | cannot find symbol |
Reading an instance field from main | compile error | non-static variable x cannot be referenced from a static context |
| Reading a local that some path leaves unassigned | compile error | variable x might not have been initialized |
| A field initializer reading a field declared below it | compile error | illegal forward reference |
| Redeclaring a local in the same or a nested block | compile error | variable x is already defined in method ... |
Writing name = name; in a setter | compiles, does nothing, no warning | none |
| Declaring an accumulator inside the loop body | compiles, resets every iteration | none |
| Shadowing a field by accident, then reading the local | compiles, wrong value | none |
The last three produce no diagnostic at all, and they share a single cause: the name resolved to something, just not to the variable you meant. When a value is wrong rather than missing, the first question to ask is which declaration the name actually refers to.
FAQ
What is the difference between a local variable and a global variable in Java?
Java has no global variables. A local variable is declared inside a method or block and exists only there; the closest equivalent to a global is a static field, which is declared in a class body, has one copy per class, and is reachable as ClassName.field from anywhere that can see the class. It still belongs to that class, so it is namespaced rather than global.
Why does Java say cannot find symbol for my loop counter?
Because a variable declared in a for header belongs to the loop and ceases to exist at its closing brace, so after the loop the name is not defined. Either declare the variable before the loop and leave the header's initializer empty, or restructure so the counter is not needed afterwards.
Can a local variable have the same name as a field in Java?
Yes. The local shadows the field for the rest of its own scope, and the field is then reachable only as this.field for an instance field or ClassName.field for a static one. It compiles without a warning, so shadow deliberately or not at all.
Why is name = name not an error in Java?
Because both occurrences resolve to the nearest declaration, which is the parameter, so the statement is a legal self-assignment of the parameter to itself. The field is never touched, javac reports nothing, and -Xlint:all reports nothing either. Write this.name = name;.
When is a local variable destroyed in Java?
At the closing brace of the block that declares it, when its stack frame is discarded. A variable declared inside a loop body is destroyed and re-created on every iteration, which is why it cannot accumulate a total across iterations.
Can I declare two variables with the same name in nested blocks in Java?
No. javac rejects it with variable x is already defined in method ..., and the rule covers parameters too. Two sibling blocks that do not contain each other may reuse a name, because the first declaration has already gone out of scope before the second appears.
Conclusion
Scope in Java is decided by braces and nothing else. A local runs from its declaration to the closing brace of its block; a parameter covers the whole method body; a field covers the whole class body, which is why a method can use a field declared below it. cannot find symbol means the name does not exist at that point, non-static variable ... cannot be referenced from a static context means it exists but has no object to live in, and variable x might not have been initialized means it exists and holds nothing the compiler can vouch for.
Lifetime is the separate question of how long the storage lasts: one slot per call for a local, one per object for an instance field, one per class for a static field. Shadowing is where the two ideas collide — the name still resolves, just to the wrong slot, and name = name; is the case that costs people an afternoon. Declaring at the narrowest scope that works is what prevents most of it.
Next in this series: recursion — a method that calls itself, the base case that stops it, how each call gets its own copy of every local, and why a missing base case ends in StackOverflowError.