skip to content

How does definite assignment interact with final variables and blank final fields?

level: middleimportance: should knowfreq 48%

answer

  1. final = assigned exactly once
  2. DA before read; DU before each assignment
  3. Blank final = final with no initializer
  4. Fields: assigned by end of every constructor / static init
  5. Assigning a final in a loop fails DU (could repeat)

basics

~20 s

A final variable can be assigned exactly once. The compiler uses two flow checks: it makes sure a final is definitely assigned before you read it, and definitely UN-assigned (not yet set) before each assignment. Together these guarantee "assigned once, never reassigned."

solid answer

~50 s

`final` means a variable is assigned exactly once. The compiler enforces this with two complementary flow analyses. Definite assignment ensures the final is set before any read (same rule as for ordinary locals). Definite *unassignment* ensures the final has not already been assigned on any path before a new assignment occurs — that is what prevents reassignment. A *blank final* is a final declared without an initializer (`final int x;`); it can be assigned later, but exactly once and only where it is definitely unassigned. Blank final fields must be definitely assigned by the end of every constructor (or in an instance initializer); static blank finals by the end of static initialization. This lets you compute a final's value conditionally — e.g., assign it in different if-branches — as long as every path assigns it exactly once. The pair of checks is how Java reconciles "immutable after init" with "value may depend on logic."

code

java · 15 lines
java
class Order {
    private final String status;   // blank final field

    Order(int code) {
        // assigned exactly once on every path -> DA holds at end of ctor, DU holds at each assign
        if (code == 0) {
            status = "OK";
        } else {
            status = "FAILED";
        }
        // status = "OTHER"; // would be a compile error: x might already have been assigned (DU fails)
    }

    String status() { return status; }
}

go deeper

for a junior

Knows final means "can't change" and that a final must be initialized, ideally at declaration.

for a middle

Explains blank finals and that the compiler enforces assign-exactly-once via both a before-read and a before-assignment check; can assign a blank final conditionally.

for a senior

Names definite unassignment explicitly, knows the constructor/static-init rules for blank final fields, and connects it to effectively-final lambda capture.

for a principal

Reasons about why two dual analyses give precise diagnostics, the this(...) delegation interaction, and uses blank finals as an idiom for immutable-yet-computed state in API design.

## Background terms - **`final` variable**: a variable that may be assigned **exactly once**. After that single assignment its value (the reference or primitive) cannot change. (For object references, the *object's* contents can still change; `final` freezes the reference, not the object.) - **Blank final**: a `final` declared with **no initializer**, e.g. `final int x;`. It starts unassigned and you assign it later — but still only once. - **Definite assignment (DA)**: the compile-time guarantee that a variable is set on every path before it is read (covered in the basic definite-assignment question). - **Definite unassignment (DU)**: the *mirror* analysis — the guarantee that a variable is **not yet assigned** on any path reaching a particular point. ## The two checks that make `final` work `final` is enforced entirely by flow analysis at compile time, using both checks: 1. **Before a read** of a final: it must be **definitely assigned** (so you never read an unset final). 2. **Before each assignment** to a `final` local or blank final: it must be **definitely unassigned** (so you never assign it twice). The second check is the one that produces the classic error *"variable x might already have been assigned"*. ```java final int x; if (cond) { x = 1; } else { x = 2; } // here x is definitely assigned AND was assigned exactly once on each path — OK System.out.println(x); ``` Each path assigns `x` exactly once, so DU holds at each assignment and DA holds at the read. This is why blank finals are powerful: the value can depend on logic. ```java final int x; x = 1; x = 2; // ERROR: x might already have been assigned (DU fails) ``` ## Blank final FIELDS For **instance** blank final fields, the rule is: the field must be definitely assigned **at the end of every constructor** that does not redirect to another `this(...)` constructor (and may instead be assigned in an instance initializer block). It must be definitely unassigned before each such assignment. ```java class C { private final int id; // blank final field C(boolean special) { if (special) id = 1; else id = 2; // every constructor path assigns id exactly once — OK } } ``` For **static** blank final fields, the field must be definitely assigned by the end of the **static initialization** of the class (static initializer blocks / inline assignment). ## Effectively final and lambdas A related concept: a local that is **never reassigned** is *effectively final* even without the keyword. Lambdas and anonymous/inner classes may capture only final or effectively-final locals. The compiler determines "effectively final" with the same single-assignment flow reasoning, so understanding DA/DU directly explains why a captured variable you reassign elsewhere causes "local variables referenced from a lambda must be final or effectively final." ## Why two analyses instead of one DA alone guarantees *initialized before use*; DU alone guarantees *not-yet-assigned*. `final` needs **both**: assigned before read **and** assigned at most once. Splitting them lets the compiler give precise, separate diagnostics ("might not have been initialized" vs. "might already have been assigned") and lets blank finals be assigned conditionally as long as exactly-once holds on every path. ## Common pitfalls - Assigning a blank final in only some branches → DA fails at later read. - Assigning a blank final in a loop body → DU fails, because a second iteration could assign it again. - Forgetting that a constructor that delegates via `this(...)` must NOT also assign the final (the delegated-to constructor already did, so DU would fail).

  • Why does assigning a blank final inside a loop body fail to compile?
    Because the loop could execute the body more than once, the variable is not definitely unassigned on the second iteration's path — so the assignment might re-assign an already-assigned final, which violates assign-once.
  • What's the difference between final and effectively final?
    final is the keyword forbidding reassignment; effectively final describes a local that is never reassigned even without the keyword. Lambdas can capture either, because both are single-assignment as proven by the same flow analysis.
  • Must a blank final field be set in every constructor?
    Yes — by the end of every constructor that doesn't delegate via this(...), or in an instance initializer. A constructor delegating with this(...) must NOT assign it, since the target already did.

A blank final is a one-time-use stamp: definite assignment makes sure the document is stamped before it leaves, and definite unassignment makes sure it isn't stamped twice. You may decide which stamp to use based on the document, but only one stamp lands.

saying these in an interview costs you the question

  • Saying final makes the referenced object immutable — it only freezes the reference/primitive
  • Thinking a blank final can be assigned in just one branch and read afterward
  • Forgetting the second check (definite unassignment) exists — that's what blocks reassignment
  • Claiming you can assign a final inside a loop body

context