JAVA / VARIABLES, PRIMITIVES AND TYPES
Variables, declarations and final constants
Declare, initialize and scope Java variables correctly, and use final and static final to create write-once locals and named constants the compiler enforces.
What you will learn
- Declare a local, then assign it before its first read
- Explain why locals have no default value but fields default to 0, false or null
- Use final for write-once locals and static final for named class constants
- Spot scope errors: a local name ends at the closing brace of its block
Understanding Variables, declarations and final constants
A declaration makes three decisions at once: the type of value the slot may hold, the name you will use to reach it, and the block the name lives in. int active; reserves a slot that can only ever hold an int, and that type is fixed for the life of the variable — no later assignment can widen or change it. The name is visible from its declaration line down to the closing brace of the enclosing block, which is why a counter declared in a for header simply stops existing after the loop.
Assignment is a separate act from declaration, and for local variables Java refuses to invent a starting value. Fields are given one automatically (0, false or null), but reading a local before its first assignment is a compile error rather than a leftover value from memory as it would be in C. The check is a flow analysis: javac walks every path that can reach the read and demands an assignment on each of them. That analysis is deliberately conservative, so it rejects code whose safety depends on runtime values it cannot see.
final marks a slot as write-once. On a local it can be initialized on the declaration line, or left blank and assigned exactly once later, which lets a branch pick the value while still fixing it forever afterwards. On a static field it is how you name a constant: static gives one copy for the whole class, final stops anyone rewriting it, and the SCREAMING_SNAKE_CASE name is the convention that signals both to a reader. What final never does is freeze whatever the value refers to — it guards the slot, not the object sitting in it.
public class Declarations {
static final int MAX_USERS = 50; // one shared, write-once constant
public static void main(String[] args) {
int active; // the slot exists, nothing stored in it yet
active = 12; // first write, and it happens before any read
final int remaining = MAX_USERS - active;
// remaining = 0; // rejected by javac: final means write once
System.out.println("active = " + active);
System.out.println("remaining = " + remaining);
active = active + 3; // an ordinary variable can be rewritten
System.out.println("active = " + active);
System.out.println("remaining = " + remaining);
}
}A declaration fixes a variable's type and scope, assignment fills it, and final is a compiler-checked promise that the fill happens exactly once.
Worked examples
Scope ends at the brace
Shows that a variable declared inside a block disappears when the block closes, freeing the name for reuse.
public class Scope {
public static void main(String[] args) {
int total = 0;
for (int i = 1; i <= 3; i++) {
int doubled = i + i;
total = total + doubled;
System.out.println("i=" + i + " doubled=" + doubled);
}
System.out.println("total=" + total);
int i = 99; // legal: the loop's i is long gone
System.out.println("new i=" + i);
}
}Example explained
Line 1doubled is declared inside the loop body, so a fresh slot appears on each pass and dies at the body's closing brace.
Line 2total is declared outside the loop, so one slot survives all three passes and accumulates into 12.
Line 3int i = 99; compiles because the for header's i was scoped to the for statement only.
Line 4Moving that declaration above the loop instead would make int i = 1 in the header a duplicate-variable error; Java forbids one local shadowing another.
A final assigned later
Demonstrates a blank final: declared with no value, assigned exactly once by a branch.
public class BlankFinal {
public static void main(String[] args) {
int hour = 21;
final String greeting; // declared, deliberately not assigned yet
if (hour < 12) {
greeting = "good morning";
} else {
greeting = "good evening";
}
System.out.println(greeting + ", it is " + hour + ":00");
}
}Example explained
Line 1final String greeting; compiles because final requires exactly one assignment, not an assignment on the declaration line.
Line 2Both branches assign it, so javac can prove the variable is initialized on every path and written at most once.
Line 3Delete the else branch and the println stops compiling: on the false path greeting would still be empty.
Line 4The value is only known at runtime, yet greeting is still final — final restricts writes, not when the value is decided.
final locks the slot, not the contents
Shows that a final variable cannot be reassigned while the object it refers to remains fully mutable.
public class FinalSlot {
public static void main(String[] args) {
final int[] scores = {10, 20, 30};
scores[0] = 99; // allowed: this writes inside the array
// scores = new int[3]; // rejected: this would rewrite the slot
System.out.println(scores[0] + "," + scores[1] + "," + scores[2]);
final int size = scores.length;
System.out.println("size=" + size);
}
}Example explained
Line 1final int[] scores fixes which array the name refers to, so any second scores = ... is a compile error.
Line 2scores[0] = 99; changes a cell inside that array, which the variable's finality says nothing about.
Line 3final int size copies the current length into its own slot; nothing keeps that copy in sync with the array afterwards.
Important notes
final on a local is a compile-time promise only: it adds no bytecode and no runtime check, so there is never a performance argument for leaving it off.
A static final int or String initialized from a constant expression gets inlined into every class that reads it, so changing 50 to 60 and recompiling only the constant's class leaves stale values in the callers — recompile them too.
Common mistakes
Writing int total; then printing total, expecting 0 because fields behave that way. Locals have no default, so javac stops with "variable total might not have been initialized" and nothing runs at all.
Assigning a blank final inside an if with no else, then adding a second assignment as a fix. The first version fails with "might not have been initialized" and the second with "variable might already have been assigned"; the real fix is one assignment per path.
Reading final on a reference as "the value can no longer change". A final array or StringBuilder is still fully mutable, so shared state keeps changing while the declaration looks locked down.
Try it yourself
Change, predict, then run
In one class, declare static final int SECONDS_PER_MINUTE = 60, a plain int minutes that you assign on the line after its declaration, and a blank final String label that you assign inside both branches of an if/else on minutes, then print all three. Now delete the else branch and read the exact error javac reports.
Open the Java workspaceCheck your understanding
A method contains: final int a; if (args.length > 0) { a = 1; } System.out.println(a); What happens?
- It fails to compile, because a is not assigned on the path where the condition is false
- It compiles and prints 0 when no arguments are passed, since an unassigned final int starts at 0
- It fails to compile, because a final variable must be assigned on its declaration line
- It compiles, and throws an exception at runtime when no arguments are passed
Show answer
The compiler requires definite assignment on every path that reaches a read, and the implicit else path assigns nothing, so the println is rejected. Option 2 is the tempting one, but blank finals are perfectly legal: the rule is exactly one assignment before use, not an assignment on the declaration line.