skip to content

How can a ternary expression throw a NullPointerException even though neither branch is dereferenced? Explain the Integer/int unboxing trap.

level: seniorimportance: must knowfreq 60%

answer

  1. Primitive + wrapper branches → wrapper gets unboxed
  2. Unboxing null → NullPointerException (no visible dot)
  3. Map.get(...) returning null inside a ternary is the classic trap
  4. Fix: make both branches the same wrapper type, OR null-guard first
  5. Result type is fixed for the whole expression

basics

~20 s

If one branch is a primitive (like int) and the other is a wrapper (like Integer), Java makes both numeric, so it unboxes the wrapper. If that wrapper is null, unboxing it throws a NullPointerException — even though you only have a number on the other side.

solid answer

~50 s

Because a ternary has one result type, mixing a primitive branch with a wrapper branch forces the compiler to find a common numeric type, which triggers **binary numeric promotion** and therefore **auto-unboxing** of the wrapper to its primitive. The classic case is `cond ? primitiveInt : nullableInteger`: the result type becomes `int`, so the `Integer` branch is unboxed via `intValue()` — and if it is `null`, that call throws `NullPointerException`. The trap is subtle because the NPE happens at the implicit unboxing, not at any explicit method call you wrote, and it can fire even when the chosen branch is the non-null primitive (the type is fixed for the whole expression). A famous example is `Map.get` returning `null`: `boolean b = cond ? map.get(k) : false;` will NPE when `map.get(k)` is null because the expression type is `boolean` and the `Boolean` gets unboxed. Fix it by keeping both branches the same reference type (e.g. both `Integer`) or guarding the null explicitly.

code

java · 15 lines
java
Map<String, Boolean> flags = new HashMap<>();
boolean cond = true;

// DANGER: result type is boolean, so the Boolean from get() is unboxed.
// If the key is missing, get() returns null -> NPE, with no visible '.' you wrote.
// boolean on = cond ? flags.get("missing") : false;  // throws NullPointerException

Integer boxed = null;
// int r = cond ? 0 : boxed;          // would unbox boxed -> NPE if that branch ran

// SAFE option 1: keep both branches the same wrapper type (no unboxing here)
Integer safe = cond ? Integer.valueOf(0) : boxed;   // safe -> just an Integer (maybe null)

// SAFE option 2: null-guard before unboxing
int r2 = (boxed != null) ? boxed : 0;               // 0, no NPE

go deeper

for a junior

May not know this trap; should at least recall that wrappers can be null and unboxing null throws NPE.

for a middle

Can explain that mixing a primitive and a wrapper unboxes the wrapper and that unboxing null throws, given a hint.

for a senior

Independently identifies the trap, cites the Map.get/false example, and gives correct fixes (same wrapper type or explicit null guard); knows the result type is fixed for the whole expression.

for a principal

Connects it to API design (avoid nullable wrapper returns), static-analysis/lint rules to catch it, and team conventions; can explain the compiler's inserted intValue() and JLS promotion rationale.

## The setup Java has two parallel worlds for numbers: **primitives** (`int`, `long`, `boolean`, …) which are raw values, and **wrapper classes** (`Integer`, `Long`, `Boolean`, …) which are *objects* that hold one of those values and can also be `null`. **Autoboxing** is the compiler automatically converting a primitive to its wrapper (`int`→`Integer`); **auto-unboxing** is the reverse (`Integer`→`int`), which the compiler implements by inserting a call like `theInteger.intValue()`. A `null` reference has no `intValue()` to call, so **unboxing a `null` wrapper throws `NullPointerException`**. ## Why the ternary forces unboxing A ternary expression must have **one result type**, computed at compile time from both branches. When one branch is a primitive and the other is the matching wrapper, the JLS rules apply **binary numeric promotion**, which operates on *primitives*. To do that, the wrapper branch must be **unboxed** to its primitive first. So the compiler rewrites: ```java Integer boxed = null; int result = cond ? 0 : boxed; ``` roughly into: ```java int result = cond ? 0 : boxed.intValue(); // boxed.intValue() -> NPE if boxed is null ``` The whole expression's type is `int`, so the `Integer` branch is unboxed. If `boxed` is `null` **and that branch is taken**, you get an NPE that points at a line where you never wrote `.intValue()`. ## The extra-nasty variant Because the *type* is fixed for the whole expression, the unboxing conversion is part of the expression's shape. In practice the unboxing throws when the **null branch is evaluated**. The truly surprising cases are the ones where you did not even realize a wrapper was involved: ```java Map<String, Boolean> flags = new HashMap<>(); // flags.get("x") returns Boolean, and returns null for a missing key boolean on = someCond ? flags.get("x") : false; ``` Here `flags.get("x")` is a `Boolean`. The other branch is the primitive `false` (`boolean`). The result type is `boolean`, so the `Boolean` branch is unboxed. If the key is missing, `get` returns `null`, unboxing throws **NullPointerException**, and the line looks completely innocent. ## Why people miss it 1. There is **no visible dereference** — no `.` you typed. 2. It can be hidden behind generics (`Map<…, Integer>`, `Optional`-less getters, ORM fields typed as `Integer`). 3. It depends on *both* branches' static types, not on the value you think you are returning. ## How to avoid it - **Keep both branches the same reference type.** `cond ? Integer.valueOf(0) : boxed` keeps the result type `Integer`, so **no unboxing happens** and you safely get a (possibly null) `Integer` — no NPE at the ternary (you defer the null handling). - **Guard the null explicitly** before unboxing: `int r = (boxed != null) ? boxed : 0;` — now both branches are effectively `int` but you have ensured the wrapper is non-null on the branch that unboxes. - **Be deliberate about the target type.** If you want a primitive result, make sure every branch can produce a non-null value. - **Watch wrapper-returning APIs** (`Map.get`, JDBC/ORM nullable columns, parsed JSON) inside ternaries. ## Mental checklist for a ternary 1. Are the two branches different types? 2. Is one a primitive and one a wrapper? 3. If so, the wrapper **will be unboxed** — can it be `null`? If yes, you have a latent NPE. This is one of the most common 'looks fine, NPEs in production' Java bugs, which is why it is asked about often.

  • Why does boolean b = cond ? map.get(key) : false; risk an NPE?
    map.get returns a Boolean (a wrapper) and the other branch is the primitive false, so the result type is boolean and the Boolean is auto-unboxed. If get returns null for a missing key, unboxing null throws NullPointerException.
  • How do you make cond ? 0 : nullableInteger NPE-safe while still returning a possibly-null value?
    Make both branches the same wrapper type, e.g. cond ? Integer.valueOf(0) : nullableInteger. Then the result type is Integer, no unboxing occurs, and you safely get an Integer (which may be null) that you handle later.

It's like a customs checkpoint that only lets through plain cash, not sealed envelopes. If one lane hands over cash (primitive) and the other an envelope (wrapper), the guard insists on opening every envelope to get the cash inside. If an envelope is empty (null), opening it 'fails' — the NPE — even though the other lane's cash was perfectly fine.

saying these in an interview costs you the question

  • Saying a ternary can't NPE because there is no explicit dereference
  • Thinking the NPE only happens when you call a method on the result
  • Assuming the non-null primitive branch protects you (the type is fixed for the whole expression)
  • Not recognizing wrapper-returning APIs (Map.get, nullable ORM columns) as the source

context