Compile-Time Errors in Java
Definite assignment requires the Java compiler to determine that a variable has been assigned before its value is used, and this is a compile-time judgment about the code's structure.
A Switch That Looks Complete
A switch statement is often the natural tool when a program must choose one of several alternatives. Suppose a program picks a random move for the computer in a game of Rock, Paper, Scissors. The random expression used to make the choice can only produce three integers, and each case in the switch handles one of them. On paper, the switch seems to account for every possibility. Yet a switch like this can still produce a compile-time error, and the reason is not a missing case. It is a rule about how Java tracks variables.
This article looks at that rule, called definite assignment, and at the construct that resolves the problem in this situation: the default case. The central idea is that the compiler does not reason about the values your program can actually produce. It reasons about what it can prove from the structure of the code.
Before reading on, try to decide for yourself. Consider the following switch. It has three cases, one for each value the random expression can produce, and the variable computerMove is printed after the switch. Will the compiler accept this code? Think about what the compiler can and cannot prove about computerMove at the point where it is printed.
What do you think happens?
Will the code above compile, given that the three cases cover every value random.nextInt(3) can produce?
Reveal answer
No, it does not compile. The compiler does not conclude that the cases cover every possible integer value, so it cannot prove computerMove is assigned before the print statement runs.
The source describes this exact situation: the compiler sees an integer-valued expression, sees that the listed cases do not obviously cover every integer, and treats the variable as possibly unassigned. A missing case is not the cause. The cause is the rule about what the compiler can prove.
How the Compiler Tracks Assignment
Definite assignment requires the Java compiler to determine that a variable has been assigned before its value is used. This is a compile-time judgment about the code's structure, not a observation of what happens when the program runs.
The word that matters in this definition is determine. The rule does not ask whether the variable happens to be assigned in the run you are watching. It asks whether the compiler can be certain, from the code itself, that an assignment has occurred on every route that leads to the use. Definite assignment was introduced earlier in the course, and the switch statement is one place where it becomes easy to violate without noticing.
Think of the compiler as walking every route the program could take from the variable's declaration to its use. If any route reaches the use without passing through an assignment, the compiler cannot confirm definite assignment, and it reports a compile-time error. It does not matter that the route might never be taken with the values the program actually receives. What matters is whether the compiler can prove that every route is safe.
Tracing Paths Through the Switch
In a switch, the compiler looks at the expression and at the case labels attached to it. In the Rock, Paper, Scissors code, the expression is an integer-valued expression and the cases name 0, 1, and 2. Those three labels cover the values the expression is known to produce, yet the compiler does not treat that as a guarantee. Because it cannot prove full coverage of all integers, it cannot prove that computerMove has been assigned.
The diagram below traces the routes through the switch. Each case is a route that assigns computerMove. The route at the bottom is the one the compiler must also account for: control can arrive at the print statement without any case having run. The compiler sees that this route exists, because no default case closes it off.
Only the route labeled skips assignment is the problem. The three named cases each assign computerMove, but the compiler does not know that the expression can never leave the listed labels. Since that route exists in its model, the variable is treated as possibly unassigned at the print statement.
Adding a Default Case
The fix the source offers is to replace the final case with a default. A default case is a branch that handles every value not named by the earlier cases. With a default in place, every possible value of the expression is certainly covered by some branch, and each branch assigns computerMove. The compiler can now confirm that the variable is definitely assigned before the print statement runs.
switch (random.nextInt(3)) {
case 0:
computerMove = 0;
break;
case 1:
computerMove = 1;
break;
case 2:
computerMove = 2;
break;
}The final case is case 2, so one route can skip every assignment.
switch (random.nextInt(3)) {
case 0:
computerMove = 0;
break;
case 1:
computerMove = 1;
break;
default:
computerMove = 2;
break;
}The final case is replaced by default, so every value reaches an assignment.
Notice what changed. The number of named cases did not change in any meaningful way, and the body that assigns computerMove is the same. The only edit is the label of the final branch. Replacing case 2 with default closes the route that previously reached the print statement without an assignment. The before and after diagram below shows the same idea as a route-by-route change.
Compile-Time Judgment Versus Runtime Values
A common assumption goes like this: if the programmer has written a case for every value the expression can take, the compiler must know that every path assigns the variable. The source identifies this belief as a misconception. The compiler is not expected to determine that all possible integer values are covered by the cases in a switch statement. It sees an integer-valued expression, sees that the listed cases do not obviously cover every integer, and treats the variable as possibly unassigned.
This explains why code can fail to compile even when every value the program will ever see would be assigned. The programmer's knowledge of the value range is real, but it is not part of the code the compiler checks. The comparison below separates what the compiler reasons about from what happens during a run.
The left column holds the only facts the compiler uses: the structure of the code, the expression's type, and the labels attached to it. The right column holds facts about execution. A program might never produce a value outside the labels, but the compiler cannot prove that from the structure alone, so it reports the error anyway.
Worked Example: A Switch on a Different Range
The same reasoning applies to any switch that assigns a variable, not just the random example. The following problem is an original illustration built on the same principle. A switch chooses a label based on an integer, and the cases cover only the values 1 and 2.
Will this switch compile?
An int named level is passed to a switch. Case 1 assigns the label to "low" and case 2 assigns it to "high". The label is printed after the switch. Does the code compile?
- 1Identify the assigned variable
The variable label is assigned inside each case and then used by the print statement after the switch.
- 2List the routes
One route goes through case 1, one through case 2, and one route is taken when level is neither 1 nor 2. That third route reaches the print statement with no assignment.
- 3Check what the compiler can prove
The compiler sees an int-valued expression and two named labels. It does not conclude that 1 and 2 cover every int, so it cannot prove label is assigned.
- 4Apply the fix
Replacing case 2 with default makes the second assignment handle every value not named by case 1. Every route now assigns label before the print statement.
As written, without a default, the code does not compile. With case 2 replaced by default, every route assigns label, and the code compiles.
- The default branch handles every int not equal to 1, so no route skips the assignment.
- The print statement is now reached only after label has been assigned on every path.
Mistakes When Reading Switch Assignments
Common mistake Assuming that covering every value the programmer expects is enough for the compiler
What it looks likeThe Rock, Paper, Scissors switch has cases 0, 1, and 2, and the programmer knows nextInt(3) only produces those values, so the programmer expects it to compile.
Why it failsThe compiler does not conclude that all possible integer values are covered by the cases. It only checks what it can prove from the structure of the code.
Do this insteadAdd a default case so that every value not named by the earlier cases reaches an assignment.
Common mistake Judging compile success by the run you are watching
What it looks likeA program runs successfully with the values it happens to receive, so the programmer concludes the switch is safe.
Why it failsDefinite assignment is about whether the compiler can be certain from the code itself. A successful run does not establish that.
Do this insteadAsk whether every route from the declaration to the use passes through an assignment.
Common mistake Treating the final named case as the last route to check
What it looks likeIn a switch with cases 1 and 2 only, the programmer checks the bodies of case 1 and case 2 and stops there.
Why it failsA route can skip every case when no label matches, and that route also reaches the use without an assignment.
Do this insteadCheck the route where no case matches, and close it with a default if one is needed.
Common mistake Forgetting that a variable assigned in only some cases is not definitely assigned after the switch
When you write a switch that assigns a variable used later, ask whether every route assigns it. If you cannot point to a branch for each possible value, or you are relying on your own knowledge of the value range, a default case states the coverage in a form the compiler can check.
Practice: Predict Before You Check
A switch on the integer score has case 0 assigning grade to 'F', case 1 assigning grade to 'P', and no other cases. The variable grade is printed after the switch. Predict whether this compiles. Then decide what single change would make it compile, and explain which route the change closes.
Hints
- Ask which values of score reach the print statement without passing through any case body.
- The compiler checks routes, not the values a program is likely to receive.
- A default case handles every value that no named case handles.
Summary
- 1Definite assignment is a compile-time check that a variable is assigned before its value is used, judged from the structure of the code.
- 2A switch whose cases look exhaustive can still fail the check, because the compiler does not conclude that the listed cases cover every integer value.
- 3Replacing the final case with a default ensures every possible value reaches an assignment, so the compiler can confirm definite assignment.
- 4When a switch assigns a variable used later, check that every branch, including a default, assigns it before the use.
At a glance
- Definite assignment requires the compiler to determine, from code structure, that a variable is assigned before it is used.
- A switch without a default case can fail this check even when its cases look exhaustive, because the compiler does not prove that all integer values are covered.
- Replacing the final case with a default closes the route that could skip assignment, so the compiler can confirm the variable is definitely assigned.
- Predict compilation by tracing every route to the use, not by considering only the values the program is expected to receive.