Concepts / Variables and Definite Assignment

Variables and Definite Assignment

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. Imagine a program that 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.

The rule is called definite assignment. It determines whether a variable is guaranteed to have been given a value before the program reads it. This article traces how the compiler applies that rule to a switch, why a switch with three cases can still fail the check, and how a default case resolves the problem.

EASY

Before reading on, decide for yourself. Suppose a switch on a random integer has cases for 0, 1, and 2 that each assign computerMove, and then 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.

Hints
  • Ask whether the compiler knows the random expression can only produce 0, 1, and 2, or whether it only sees an integer-valued expression.
  • Ask whether there is a path through the switch on which no case runs and computerMove is never assigned.

Following the Assignment Through Each Branch

To see why the compiler objects, follow the paths through the switch. Each case assigns computerMove and then leaves the switch. The variable is printed after the switch, so every route from the start of the switch to the print statement must pass through an assignment for the code to be safe. The diagram below shows the routes that exist in the code as written.

switch on randomvaluecase 0 assignscomputerMovecase 1 assignscomputerMoveprint computerMovecase 2 assignscomputerMoveno matching casecomputerMove not assigned012any other integeruse without assignment
Which paths through the switch reach the print statement without computerMove ever being assigned, and where does the compiler reject the code?

Three of the four routes into the print statement pass through an assignment. The fourth route, the one where no case matches, does not. The diagram shows the uncovered route as an edge, and that edge is what the compiler cannot rule out. The source describes the situation directly: 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.

Why the Compiler Judges Structure

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 check of the values the program happens to produce when it runs.

The key word in the 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. The random expression in the example really does produce only 0, 1, or 2 when the program runs. The compiler, however, does not reason about the values the program can actually produce. It reasons about what it can prove from the structure of the code, and the structure contains a switch whose listed labels do not cover every integer.

This explains a common assumption that the source identifies as a misconception. A programmer may reason that if a case exists for every value the expression can take, the compiler must know that every path assigns the variable. The compiler is not expected to determine that all possible integer values are covered by the cases in a switch statement. So a switch whose cases look exhaustive to a human can still fail the check, because the compiler does not reach the same conclusion from the labels alone.

In the real world

A game program chooses the computer's move with a random integer and then prints the move. A reader might say the code is correct because the random number can only be 0, 1, or 2. The compiler, however, sees only an integer-valued expression and three labels. It cannot prove full coverage, so it cannot prove that computerMove has been assigned before the print statement. The fix is not to change the random number. The fix is to state the coverage in the code.

Adding a default Branch to Close the Gap

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 covered by some branch, and each branch assigns computerMove. The compiler can then confirm that the variable is definitely assigned before the print statement runs. The code below shows the version without a default first, and then the version where the final case has become a default.

Before
int choice = (int) (Math.random() * 3);
String computerMove;
switch (choice) {
    case 0:
        computerMove = "Rock";
        break;
    case 1:
        computerMove = "Paper";
        break;
    case 2:
        computerMove = "Scissors";
        break;
}
System.out.println(computerMove);

Three labeled cases with no branch for any other integer. The compiler cannot prove computerMove is assigned at the print statement, so the code is rejected.

After
int choice = (int) (Math.random() * 3);
String computerMove;
switch (choice) {
    case 0:
        computerMove = "Rock";
        break;
    case 1:
        computerMove = "Paper";
        break;
    default:
        computerMove = "Scissors";
        break;
}
System.out.println(computerMove);

The final case is now a default that assigns computerMove. Every value reaches an assignment, so the compiler can confirm definite assignment and the code compiles.

Code walkthrough
String computerMove; declares the variable without a value. Each case assigns it and then uses break to leave the switch. The default branch in the second version catches any integer that none of the earlier labels named, so there is no route from the switch to the print statement that skips an assignment.
  1. 1
    The declaration String computerMove; gives the variable a name and type but no value.
  2. 2
    The switch reads choice and jumps to the matching case label.
  3. 3
    Each named case assigns computerMove and then breaks out of the switch.
  4. 4
    In the after version, default assigns computerMove when no named label matches.
  5. 5
    After the switch, every route has assigned computerMove, so the print statement is safe to compile.
  • String computerMove; declares the variable without assigning it
  • break ends each case so control does not fall through to the next label
  • default handles every value not named by the other cases
case 0assigns computerMovecase 0assigns computerMovecase 1assigns computerMovecase 1assigns computerMovecase 2assigns computerMovedefaultassigns computerMoveother integersno assignment
How does adding a default branch change which paths assign the variable, and why does the same set of cases compile in one version but not the other?

The comparison makes the difference concrete. In the version without a default, the integers outside the labeled cases have a route that ends in no assignment. In the version with a default, that route is taken by the default branch, which assigns computerMove. The compiler's judgment changes because the code structure changed, even though the program's behavior for the three named values is the same.

Applying the Same Reasoning to a New Switch

The same reasoning applies to any switch that assigns a variable, not just the random example. Consider an original illustration built on the same principle. A switch chooses a label based on an integer, and its cases cover only the values 1 and 2. If the chosen label is printed after the switch, the compiler must again decide whether every route assigns the variable. Because the cases do not account for every integer, the compiler cannot prove the assignment, and the same error pattern appears. Adding a default that assigns the label would close the gap.

Worked example

Deciding whether a switch assigns a variable on every path

A switch on an integer named level has case 1 and case 2 assigning a String named label, with no default. The program then prints label. Does the compiler accept it?

  1. 1
    Identify the variable and the use

    The variable is label, and the use is the print statement after the switch.

  2. 2
    List the routes that reach the use

    A route exists through case 1, a route exists through case 2, and a route exists when level matches neither label.

  3. 3
    Check each route for an assignment

    The first two routes assign label. The third route, where no case runs, does not assign it.

  4. 4
    Apply the compile-time rule

    The compiler cannot prove that every integer is covered by the cases, so it cannot prove label is definitely assigned. The code is rejected.

Result

The code does not compile. Adding a default case that assigns label would make every route assign the variable.

Mistakes That Trigger the Error

  • Common mistake

    Assuming that cases covering every value the expression can produce satisfies the compiler

    What it looks like

    A random integer is known to be 0, 1, or 2, and the switch has cases for exactly those three values, but the variable is printed afterward.

    Why it fails

    The compiler does not conclude that all possible integer values are covered by the cases. It sees an integer-valued expression and treats the variable as possibly unassigned.

    Do this instead

    Add a default case that assigns the variable, so every value reaches an assignment in the code's structure.

  • Common mistake

    Judging definite assignment by what happens in one run of the program

    What it looks like

    The program runs once, takes case 2, and prints the variable without error, so the developer concludes the code is safe.

    Why it fails

    Definite assignment asks whether the compiler can be certain that an assignment occurred on every route. One successful run does not establish that.

    Do this instead

    Check every route that reaches the use, including routes that no test run has happened to take.

  • Common mistake

    Forgetting that a case without an assignment still leaves a route open

    What it looks like

    One case assigns the variable and another case only prints something, and both lead to a later use of the variable.

    Why it fails

    The route through the case that does not assign the variable reaches the use with the variable unassigned.

    Do this instead

    Make sure every branch that can reach the use assigns the variable, including a default if one is present.

Predict Before You Check the Switch

What do you think happens?

Will the compiler accept the switch with cases 0, 1, and 2, and no default, when computerMove is printed after the switch?

  • Yes, because the cases cover every value the expression can produce

  • No, because the compiler cannot prove that every integer is covered by the cases

Reveal answer
Answer:

No, because the compiler cannot prove that every integer is covered by the cases

The compiler sees an integer-valued expression and does not treat the three labels as full coverage of all integers. It therefore treats computerMove as possibly unassigned at the print statement and rejects the code. Adding a default case that assigns computerMove makes every route assign it.

MEDIUM

Take a switch on an integer with cases 1 and 2 that each assign a variable named result, and then print result. Rewrite the switch so the compiler can confirm result is definitely assigned, and explain in one sentence which branch makes the difference.

Hints
  • Look for the branch that catches every integer not named by the earlier cases.
  • The branch you add must assign result, or a route will still reach the print statement unassigned.

Key Takeaways

  • 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.
  • The compiler does not conclude that all possible integer values are covered by the cases in a switch, so a switch whose cases look exhaustive can still fail the check.
  • A default case handles every value not named by the earlier cases, so when each branch assigns the variable, the compiler can confirm definite assignment.
  • When a switch assigns a variable used later, check that every branch, including a default, assigns it, rather than relying on your own knowledge of the value range.