Concepts / Compile-Time Errors in Java

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.

  • Programming

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.

java

What do you think happens?

Will the code above compile, given that the three cases cover every value random.nextInt(3) can produce?

  • Yes, because the cases cover every possible value

  • No, because the compiler cannot prove computerMove is assigned on every path

Reveal answer
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

Definition

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.

switch on randomvaluecase 0: assign 0print computerMovecase 1: assign 1case 2: assign 2no matching case012other valueskips assignment
Which path through the switch reaches the print statement without ever assigning computerMove, and why does that path exist even though the listed cases look exhaustive?

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.

Before
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.

After
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.

case 2 (final)defaultunmatched value skipsassignmentevery route assignscomputerMove
How does control flow change when the final branch becomes a default, and which routes now all end in an assignment?

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.

code structurevalues actuallyproducedinteger-valuedexpressionone specific runlisted case labelsassignment thatoccurred
How does the compiler's check differ from what actually happens at runtime, so that code can fail to compile even when every value the program will ever see would be assigned?

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.

Worked example

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?

  1. 1
    Identify the assigned variable

    The variable label is assigned inside each case and then used by the print statement after the switch.

  2. 2
    List 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.

  3. 3
    Check 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.

  4. 4
    Apply 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.

Result

As written, without a default, the code does not compile. With case 2 replaced by default, every route assigns label, and the code compiles.

java
Notes on the code
  • 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 like

    The 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 fails

    The 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 instead

    Add 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 like

    A program runs successfully with the values it happens to receive, so the programmer concludes the switch is safe.

    Why it fails

    Definite assignment is about whether the compiler can be certain from the code itself. A successful run does not establish that.

    Do this instead

    Ask 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 like

    In a switch with cases 1 and 2 only, the programmer checks the bodies of case 1 and case 2 and stops there.

    Why it fails

    A route can skip every case when no label matches, and that route also reaches the use without an assignment.

    Do this instead

    Check 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

Best practice

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

MEDIUM

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

  1. 1
    Definite assignment is a compile-time check that a variable is assigned before its value is used, judged from the structure of the code.
  2. 2
    A switch whose cases look exhaustive can still fail the check, because the compiler does not conclude that the listed cases cover every integer value.
  3. 3
    Replacing the final case with a default ensures every possible value reaches an assignment, so the compiler can confirm definite assignment.
  4. 4
    When 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.