Java never lets a program read a local variable that has not been given a value, and
never lets a final variable be assigned twice. The compiler proves both
with a flow analysis called definite assignment (JLS chapter 16). The analysis
is deliberately simple: it does not evaluate conditions, except for constant
expressions, so it can reject code that would in fact be safe.
The two rules
| Rule | Applies to | Error when broken |
|---|---|---|
| A variable must be definitely assigned (DA) before every read | Local variables, and blank final fields in constructors and initialisers | “variable might not have been initialized” |
A final variable must be definitely unassigned (DU) before every assignment | Blank final local variables and fields | “variable might already have been assigned” |
Fields that are not blank finals, array components and parameters always have a
value, so they are not checked. A read means any use of the variable’s name except as
the left side of a simple = assignment.
int x;
if (flag) x = 1;
System.out.println(x); // error: x might not have been initialized
int y;
if (flag) y = 1; else y = 2;
System.out.println(y); // fine: assigned on both branches
final int z;
z = 1;
z = 2; // error: z might already have been assigned
Conditions and boolean operators
For boolean expressions the analysis tracks two states: what is assigned when the expression is true, and when it is false (JLS 16.1):
a && b: a variable is DA when true if it is DA afteraor afterbwhen they are true; DA when false only if it is DA whenais false and whenbis false.a || b: the mirror image.!a: swaps the two states.- The constant
truecounts as assigning every variable “when false”, because that case cannot happen;falsethe same “when true”.
int n;
if (s != null && (n = s.length()) > 0) {
use(n); // fine: n is assigned when the condition is true
}
int k;
while (true) {
k = read();
if (k >= 0) break;
}
use(k); // fine: the loop only ends through the break, after k = read()
Statements
| Statement | A variable is DA after it when |
|---|---|
if (e) S1 else S2 | DA after S1 and after S2 |
if (e) S | DA after S and DA after e when false |
while (e) S | DA after e when false, and before every break out of the loop |
do S while (e); | DA after e when false, and before every break |
switch | DA at the end of every rule or the last group, and before every break; for a switch without a default that is not exhaustive, also DA after the selector |
try … catch | DA after the try block and after every catch block |
try … finally | DA after the try (and catches), or DA after the finally block |
A statement that cannot complete normally (return, throw, break, continue, yield) | Always: every variable counts as DA (and DU) after it, since nothing follows |
Inside a catch block, a variable assigned in the try block is not DA,
because the exception may have come first. A final variable assigned in
a loop body is not DU on the next iteration, so it cannot be assigned there.
Switch expressions and yield
A variable is DA after a switch expression if it is DA before every
yield and at the end of every expression rule; for a boolean switch
expression, the analysis tracks the true and false states of each result, as for
? :.
final String kind;
switch (code) {
case 1 -> kind = "one";
case 2 -> kind = "two";
default -> throw new IllegalArgumentException();
}
use(kind); // fine: every path assigns kind exactly once
Blank final fields
- A blank
finalstatic field must be DA at the end of the static initialisers and static field initialisers, in textual order, and DU before its one assignment. - A blank
finalinstance field must be DA at the end of every constructor that does not start withthis(…), taking the instance initialisers into account. A constructor that callsthis(…)must not assign it, since the other constructor already does. - Inside the constructors and initialisers, a blank final field may be read, by its
simple name or as
this.x, only once it is DA. Reading it from a method that the constructor calls is not checked, and can see the default value. - A lambda body in a constructor or initialiser may read a blank final field only if it is DA before the lambda expression.
class Temperature {
private final double kelvin;
Temperature(double celsius) {
if (celsius < -273.15) throw new IllegalArgumentException();
kelvin = celsius + 273.15; // assigned exactly once on every normal path
}
Temperature() { this(20); } // must not assign kelvin
}
Lambdas, inner classes and effectively final
A local variable used in a lambda body, a local or anonymous class, or a
switch guard must be DA before that body, and must be
final or effectively
final. A variable declared without an initialiser can still be effectively final
if it is DU before each assignment:
String msg;
if (ok) msg = "done"; else msg = "failed"; // assigned once on each path
Runnable r = () -> System.out.println(msg); // fine: msg is effectively final