skip to content

Why does this not compile: `int x; System.out.println(x);` inside a method, even though an int field would print 0?

level: juniorimportance: must knowfreq 65%

answer

  1. locals = no default value
  2. definite assignment proven on every path
  3. 'variable might not have been initialized'
  4. field would default to 0; local won't
  5. compiler is conservative / structural, not runtime

basics

~20 s

Local variables don't get default values. The compiler requires you to assign a value before reading one, so it rejects the code with 'variable x might not have been initialized'. A field would instead default to 0.

solid answer

~40 s

Inside a method, `x` is a local variable, and Java does not give local variables a default value. The compiler performs definite-assignment analysis: it checks that on every execution path reaching a use of `x`, a value was assigned first. Here `x` is read before any assignment, so compilation fails with 'variable x might not have been initialized'. If instead `x` were a field of a class, the JVM would default-initialize it to 0 and the read would succeed. The reasoning is intentional: reading an unset local is almost always a bug, and because the compiler can prove whether a local was assigned, it forces you to be explicit rather than silently handing back a possibly-wrong default. The fix is to initialize it, e.g. `int x = 0;`.

go deeper

for a junior

State that locals have no default and must be assigned before use, unlike fields which default to 0/null/false.

for a middle

Describe definite-assignment analysis and give if/else examples where it passes vs fails; name the exact compiler error.

for a senior

Explain that the analysis is conservative and structural (per JLS), contrast cost/benefit with field defaulting, and mention blank finals using the same mechanism.

for a principal

Relate it to language-design tradeoffs (compile-time safety vs. ergonomics), how the JLS specifies it precisely, and how tooling/static analysis builds on these guarantees.

## The two scopes in play A **field** is a variable declared in a class body (outside methods); a **local variable** is declared inside a method, constructor, or block. The question contrasts the two because they behave oppositely with respect to default values. ## Fields get defaults, locals don't When the JVM creates an object (or initializes a class for statics), it zeroes the memory, so every field starts with a guaranteed default: `0` for `int`, `false` for `boolean`, `null` for references, and so on. That is why an `int` field can be printed and yields `0`. A **local variable lives on the call stack** and is created fresh each time the method runs. Java deliberately gives it **no** default. Instead the compiler enforces **definite assignment**. ## Definite-assignment analysis This is a compile-time check defined in the Java Language Specification. Before any *read* (use) of a local variable, the compiler must be able to prove that the variable was *definitely assigned* on every path that could reach that point. If it can't prove it, you get the error 'variable x might not have been initialized'. Examples: ```java int x; // declared, not assigned System.out.println(x); // ERROR: might not have been initialized ``` but this is fine because every path assigns before the read: ```java int x; if (cond) { x = 1; } else { x = 2; } System.out.println(x); // OK ``` and this fails because one path (the else) leaves it unset: ```java int x; if (cond) { x = 1; } System.out.println(x); // ERROR ``` The analysis is conservative: it works on the *structure* of the code, not on runtime values. Even if `cond` is always true at runtime, if the compiler can't prove the variable is assigned on all paths, it rejects the code. ## Why Java does this Reading a never-assigned local is virtually always a programmer mistake. In languages without this rule, you'd silently get garbage or a zero and the bug hides. Java moves the detection to compile time, where it's cheapest to fix. Fields can't get the same treatment cheaply — objects are constructed in many ways and the compiler can't always prove field assignment — so fields instead get guaranteed safe defaults. ## The fixes - Initialize at declaration: `int x = 0;` - Or assign on all paths before the first read. ## Subtlety: `final` locals and blank finals A `final` local may be left unassigned at declaration (a 'blank final') and assigned exactly once later, as long as definite-assignment proves it's assigned before use and never reassigned. The same analysis powers both checks.

  • If you assign x in both branches of an if/else before reading it, does it compile?
    Yes — definite assignment is satisfied because every path assigns x before the read.
  • Does the analysis consider runtime values, e.g. if (true) { x = 1; }?
    It's structural and conservative; with a constant condition like if(true) the compiler can treat the branch as always taken, but for non-constant conditions it requires all paths to assign.

saying these in an interview costs you the question

  • Saying the local defaults to 0 but the println just fails for another reason — it never gets a default
  • Claiming it's a runtime error — it is a compile-time error
  • Believing assigning on only one branch of an if/else is enough
  • Confusing this with a NullPointerException

context