Java Language Specification — Variables and Scopes
Source: https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.12
1. Spec Reference
- JLS §4.12: Variables — https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.12
- JLS §4.12.1: Variables of Primitive Type
- JLS §4.12.2: Variables of Reference Type
- JLS §4.12.3: Kinds of Variables
- JLS §4.12.4:
final Variables - JLS §4.12.5: Initial Values of Variables
- JLS §4.12.6: Types, Classes, and Interfaces
- JLS §6.3: Scope of a Declaration
- JLS §6.4: Shadowing and Obscuring
- JLS §14.4: Local Variable Declaration Statements
- JLS §14.14.1: The Basic
for Statement - JLS §6.5: Determining the Meaning of a Name
- JLS §16: Definite Assignment
-- JLS §4.12: Variable Declarations --
-- Variables are declared via FieldDeclaration, LocalVariableDeclaration,
-- FormalParameter, ExceptionParameter, PatternVariable, etc.
-- JLS §8.3: Field Declarations --
FieldDeclaration:
{FieldModifier} UnannType VariableDeclaratorList ;
FieldModifier:
Annotation
public | protected | private
static | final | transient | volatile
UnannType:
UnannPrimitiveType
UnannReferenceType
VariableDeclaratorList:
VariableDeclarator { , VariableDeclarator }
VariableDeclarator:
VariableDeclaratorId [= VariableInitializer]
VariableDeclaratorId:
Identifier [Dims]
VariableInitializer:
Expression
ArrayInitializer
-- JLS §14.4: Local Variable Declaration Statements --
LocalVariableDeclaration:
{VariableModifier} LocalVariableType VariableDeclaratorList
LocalVariableDeclarationStatement:
LocalVariableDeclaration ;
LocalVariableType:
UnannType
var
VariableModifier:
Annotation
final
-- JLS §8.4.1: Formal Parameters --
FormalParameter:
{VariableModifier} UnannType VariableDeclaratorId
VariableArityParameter
VariableArityParameter:
{VariableModifier} UnannType {Annotation} ... Identifier
-- JLS §14.20: Exception Parameters --
CatchFormalParameter:
{VariableModifier} CatchType VariableDeclaratorId
CatchType:
UnannClassType { | ClassType }
-- JLS §4.12.4: Final Variables --
-- A variable is effectively final if:
-- 1. It is declared final, OR
-- 2. It is not declared final but is never assigned after initial assignment.
-- JLS §16: Definite Assignment --
-- Every local variable must be definitely assigned before its value is accessed.
-- This is tracked via flow analysis at compile time.
3. Core Rules & Constraints
3.1 Kinds of Variables (JLS §4.12.3)
Java has eight kinds of variables: 1. Class variables — static fields; exist from class initialization until unloading. 2. Instance variables — non-static fields; exist from object creation until GC. 3. Array components — unnamed; created with array; destroyed with array. 4. Method parameters — scoped to method body; new binding per invocation. 5. Constructor parameters — scoped to constructor body; new binding per invocation. 6. Lambda parameters — scoped to lambda body. 7. Exception parameters — in catch clause; scoped to catch block. 8. Local variables — declared in method body, constructor body, or initializer block.
3.2 Default Initial Values (JLS §4.12.5)
| Type | Default Value |
byte, short, int | 0 |
long | 0L |
float | 0.0f |
double | 0.0d |
char | '\u0000' |
boolean | false |
| any reference type | null |
Only class/instance variables and array components have default initial values. Local variables do NOT have defaults — using them before assignment is a compile error.
3.3 final Variables (JLS §4.12.4)
- A
final variable may be assigned exactly once. - A
final local variable can be assigned in different branches if only one executes. - A
final field with a compile-time constant is a constant variable. - Constant variables with primitive or
String types are inlined by javac. - Blank
final fields must be definitely assigned by the end of every constructor.
3.4 Scope Rules (JLS §6.3)
- Class variable / instance variable: scope is the entire class body (can refer to it before declaration textually, with restrictions on forward references in initializers).
- Method/constructor parameter: scope is the method/constructor body.
- Local variable: scope from the declaration point to the end of the enclosing block.
- For-loop init variable: scope is the for statement (init + condition + update + body).
- Exception parameter: scope is the catch block only.
- Pattern variable (Java 16+): scope is the "true" part of the enclosing
instanceof expression.
3.5 Shadowing (JLS §6.4.1)
- A local variable can shadow a field with the same name.
- Inside an instance method,
this.fieldName accesses the shadowed field. - A local variable in an inner block can shadow a local variable in an outer block.
- A local variable cannot have the same name as another local variable in the same scope.
3.6 var — Local Variable Type Inference (JLS §14.4, Java 10+)
var is a reserved type name, not a keyword; it can still be used as an identifier in older code (but should not be). var is only valid for local variables with initializers, for-loop variables, and try-with-resources variables. - The inferred type is the type of the initializer expression (not
Object). var cannot be used for fields, method return types, or parameters.
4. Type Rules
4.1 Definite Assignment (JLS Chapter 16)
- The compiler ensures every local variable is definitely assigned before use.
- Definite assignment is tracked through all control flow paths.
- A variable is definitely assigned after
if (x != null) in the then branch. - Definite assignment uses conservative analysis — false negatives may cause errors even when logically sound.
4.2 Effectively Final (JLS §4.12.4, §15.27.2)
- Local variables and parameters used in lambda expressions or inner classes must be effectively final (or explicitly
final). - A variable is effectively final if it is never assigned after initialization.
- This prevents issues with variable capture in closures.
4.3 Variable Re-use in Nested Blocks
// JLS §6.3: It is illegal to declare a local variable with the same
// name as a local variable or parameter in an enclosing scope:
void foo(int x) {
int x = 0; // compile error: x already defined
{
int x = 1; // compile error: x already defined
}
}
// But: in for-loop init, the variable is a new scope
for (int i = 0; i < 10; i++) { }
for (int i = 0; i < 10; i++) { } // OK: each for creates new scope
4.4 volatile Variables (JLS §8.3.1.4)
volatile on an instance or class variable ensures visibility across threads. - Reads and writes of
volatile variables are atomic for all types except long and double. - For
long and double: the JVM may split read/write into two 32-bit operations unless volatile. volatile does NOT make compound operations (i++) atomic.
4.5 transient Variables (JLS §8.3.1.3)
transient is a hint to serialization mechanisms to skip this field. - Has no effect on normal Java code execution.
- Cannot be
static transient and also final (the combination is pointless).
5. Behavioral Specification
5.1 Local Variable Lifetime
- Local variables are allocated on the stack frame (or heap via escape analysis).
- They are created when the block is entered and destroyed when the block is exited.
- JVM may optimize and keep locals in registers.
5.2 Field Initialization Order (JLS §12.5, §8.3.2)
- Instance variables are initialized in textual order within the class body.
- Initializers and instance initializer blocks are interleaved in textual order.
- Forward references to instance variables in instance initializers are restricted (JLS §8.3.2.3).
5.3 Thread Visibility (JLS §17.4)
- Without synchronization, changes to shared variables may not be visible to other threads.
volatile guarantees visibility but not atomicity of compound operations. synchronized guarantees both mutual exclusion and visibility (memory barrier). final fields are safely published once constructor completes (JLS §17.5).
5.4 Pattern Variable Scope (JLS §6.3.2, Java 16+)
instanceof pattern variables are scoped to the branch where the pattern holds. - In
if (obj instanceof String s), s is in scope in the if-body. - In
if (!(obj instanceof String s)), s is in scope in the else-body.
6. Defined vs Undefined Behavior
| Situation | Behavior per JLS |
| Using local variable before assignment | Compile-time error (JLS §16) |
| Instance field not explicitly initialized | Default value assigned automatically |
volatile long on 32-bit JVM | Guaranteed atomic read/write (JLS §17.7) |
Non-volatile long on 32-bit JVM | May be read/written as two 32-bit words (torn read/write) |
final field written after construction | Behavior is unspecified (data race — JLS §17.5) |
| Effectively final check | Conservative: if compiler can't prove it, it's not effectively final |
| Two local vars with same name in same scope | Compile-time error |
var with null initializer | Compile-time error (cannot infer type) |
var without initializer | Compile-time error |
7. Edge Cases from Spec
7.1 Blank Final Field
class Config {
final int timeout;
Config(boolean fast) {
if (fast) {
timeout = 100;
} else {
timeout = 5000;
}
// OK: exactly one branch assigns timeout in every constructor path
}
}
7.2 Forward Reference Restriction
class Broken {
int a = b + 1; // compile error: illegal forward reference
int b = 2;
}
class OK {
int b = 2;
int a = b + 1; // fine: b declared before a
}
7.3 var Type Inference
var list = new java.util.ArrayList<String>(); // inferred: ArrayList<String>
var n = 42; // inferred: int (NOT Integer)
var x = null; // COMPILE ERROR: cannot infer type
var y; // COMPILE ERROR: var requires initializer
7.4 Effectively Final Capture
void example() {
int x = 10; // effectively final
Runnable r = () -> System.out.println(x); // OK
int y = 10;
y++; // y is reassigned → no longer effectively final
// Runnable r2 = () -> System.out.println(y); // COMPILE ERROR
}
7.5 Pattern Variable Scope
Object obj = "hello";
if (obj instanceof String s && s.length() > 3) {
// s is in scope here — pattern match succeeded
System.out.println(s.toUpperCase());
}
// s is NOT in scope here
7.6 volatile vs synchronized
class Counter {
private volatile int count = 0;
// NOT atomic: count++ is read-modify-write
void increment() { count++; } // thread-unsafe despite volatile
// Atomic with synchronized
synchronized void safeIncrement() { count++; }
}
8. Version History
| Java Version | Change | JEP/Reference |
| Java 1.0 | All 8 variable kinds defined; definite assignment analysis | JLS 1st ed. |
| Java 1.1 | final local variables allowed | JLS 2nd ed. |
| Java 5 | Enhanced for loop variables; varargs parameters | JSR 201 |
| Java 8 | Effectively final (relaxed requirement for lambda capture) | JEP 126 |
| Java 10 | var for local variable type inference | JEP 286 |
| Java 11 | var in lambda parameters | JEP 323 |
| Java 14 | Pattern matching instanceof (preview): pattern variables | JEP 305 |
| Java 16 | Pattern matching instanceof (standard) | JEP 394 |
| Java 17 | Pattern variable scope refinements | JLS updates |
| Java 21 | Pattern matching for switch — extends pattern variable scope rules | JEP 441 |
9. Implementation-Specific Behavior (JVM-Specific)
9.1 Local Variable Storage
- HotSpot JVM stores local variables in the stack frame's local variable table.
- The JIT compiler may eliminate variables entirely (store in registers or inline constants).
- The JVM debug format (
LineNumberTable, LocalVariableTable) records variable scope for debuggers.
9.2 Escape Analysis and Stack Allocation
- HotSpot's JIT (C2 compiler) performs escape analysis.
- If an object does not escape a method, it may be stack-allocated (not heap-allocated).
- This makes
var obj = new Foo() inside a tight loop potentially free of GC pressure.
9.3 volatile Memory Model
- On x86/x64:
volatile reads are normal loads; writes use LOCK XCHG or MFENCE instructions. - On ARM:
volatile uses load-acquire / store-release barriers. - The JVM abstracts this; code is portable but performance varies.
9.4 long/double Tearing
- On 32-bit JVMs: non-volatile
long and double reads/writes may be non-atomic. - On 64-bit JVMs (modern): typically atomic even without
volatile, but not guaranteed by spec. - Always use
volatile for shared long/double fields.
10. Spec Compliance Checklist
11. Official Examples (Compilable Java 21 Code)
// Example 1: Variable Kinds Demonstration
// File: VariableKinds.java
public class VariableKinds {
// Class variable (static field)
static int instanceCount = 0;
// Instance variable
private final String name;
private int value;
// Constructor parameter
public VariableKinds(String name, int value) {
this.name = name; // 'name' is constructor parameter
this.value = value;
instanceCount++;
}
// Method with local variables and method parameter
public int compute(int multiplier) { // 'multiplier' is method parameter
int result = value * multiplier; // 'result' is local variable
return result;
}
public static void main(String[] args) { // 'args' is method parameter
VariableKinds v1 = new VariableKinds("first", 10);
VariableKinds v2 = new VariableKinds("second", 20);
System.out.println("instances: " + instanceCount); // 2
// Local variable with var (Java 10+)
var result = v1.compute(5);
System.out.println("result: " + result); // 50
// Exception parameter
try {
int[] arr = new int[0];
int x = arr[5]; // throws
} catch (ArrayIndexOutOfBoundsException e) { // 'e' is exception parameter
System.out.println("Caught: " + e.getMessage());
}
}
}
// Example 2: Scope and Shadowing
// File: ScopeDemo.java
public class ScopeDemo {
private int x = 100; // instance variable
public void scopeTest() {
int x = 200; // local variable shadows instance variable
System.out.println("local x: " + x); // 200
System.out.println("field x: " + this.x); // 100
{ // inner block
int y = 300; // y scoped to this block
System.out.println("inner y: " + y);
}
// y not accessible here
// For-loop creates its own scope
for (int i = 0; i < 3; i++) {
int loopLocal = i * 2;
System.out.println(loopLocal);
}
// i and loopLocal not accessible here
// Can reuse i in a new for-loop
for (int i = 0; i < 2; i++) {
System.out.println("second loop i: " + i);
}
}
public static void main(String[] args) {
new ScopeDemo().scopeTest();
}
}
// Example 3: final and Effectively Final
// File: FinalDemo.java
import java.util.function.Supplier;
public class FinalDemo {
// Blank final field
final int maxRetries;
FinalDemo(boolean strict) {
maxRetries = strict ? 3 : 10; // assigned in constructor
}
public Supplier<String> makeGreeter(String name) {
// 'name' is effectively final (never reassigned)
return () -> "Hello, " + name; // captures effectively final
}
public void demonstrateVar() {
// var type inference
var message = "Inferred String"; // type: String
var number = 42; // type: int
var list = new java.util.ArrayList<Integer>(); // type: ArrayList<Integer>
list.add(1);
list.add(2);
for (var item : list) { // item: Integer
System.out.println(item);
}
}
public static void main(String[] args) {
FinalDemo demo = new FinalDemo(true);
System.out.println("maxRetries: " + demo.maxRetries);
Supplier<String> greeter = demo.makeGreeter("World");
System.out.println(greeter.get());
demo.demonstrateVar();
}
}
// Example 4: Pattern Variables (Java 16+)
// File: PatternVariables.java
public class PatternVariables {
sealed interface Shape permits Circle, Rect {}
record Circle(double radius) implements Shape {}
record Rect(double width, double height) implements Shape {}
static double area(Shape shape) {
// Pattern matching instanceof (JEP 394)
if (shape instanceof Circle c) {
return Math.PI * c.radius() * c.radius();
} else if (shape instanceof Rect r) {
return r.width() * r.height();
}
return 0;
}
// Pattern matching switch (JEP 441, Java 21)
static String describe(Object obj) {
return switch (obj) {
case Integer i when i < 0 -> "negative int: " + i;
case Integer i -> "positive int: " + i;
case String s -> "string of length " + s.length();
case null -> "null value";
default -> "other: " + obj.getClass().getSimpleName();
};
}
public static void main(String[] args) {
System.out.println(area(new Circle(5.0)));
System.out.println(area(new Rect(3.0, 4.0)));
System.out.println(describe(-5));
System.out.println(describe("hello"));
System.out.println(describe(null));
}
}
// Example 5: volatile and Thread Safety
// File: VolatileDemo.java
public class VolatileDemo {
// volatile ensures visibility across threads
private volatile boolean running = true;
private volatile int counter = 0;
// For true atomicity, use AtomicInteger
private final java.util.concurrent.atomic.AtomicInteger atomicCounter
= new java.util.concurrent.atomic.AtomicInteger(0);
public void start() throws InterruptedException {
Thread worker = new Thread(() -> {
while (running) {
counter++; // NOT atomic (read-modify-write), but visible
atomicCounter.incrementAndGet(); // atomic
}
});
worker.start();
Thread.sleep(10);
running = false; // volatile write — visible to worker thread
worker.join();
System.out.println("counter (may be imprecise): " + counter);
System.out.println("atomicCounter (precise): " + atomicCounter.get());
}
public static void main(String[] args) throws InterruptedException {
new VolatileDemo().start();
}
}
| Section | Topic | URL |
| JLS §4.12 | Variables | https://docs.oracle.com/javase/specs/jls/se21/html/jls-4.html#jls-4.12 |
| JLS §6.3 | Scope of a Declaration | https://docs.oracle.com/javase/specs/jls/se21/html/jls-6.html#jls-6.3 |
| JLS §6.4 | Shadowing and Obscuring | https://docs.oracle.com/javase/specs/jls/se21/html/jls-6.html#jls-6.4 |
| JLS §14.4 | Local Variable Declarations | https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.4 |
| JLS §16 | Definite Assignment | https://docs.oracle.com/javase/specs/jls/se21/html/jls-16.html |
| JLS §17 | Threads and Locks | https://docs.oracle.com/javase/specs/jls/se21/html/jls-17.html |
| JEP 286 | Local-Variable Type Inference | https://openjdk.org/jeps/286 |
| JEP 323 | var in Lambda Parameters | https://openjdk.org/jeps/323 |
| JEP 394 | Pattern Matching instanceof | https://openjdk.org/jeps/394 |
| JEP 441 | Pattern Matching switch | https://openjdk.org/jeps/441 |