Explain why `value class Color(val rgb: Int)` boxes when its type is `Color?` but not when it's `Color`, and how this interacts with a value class whose underlying type is already a reference type.
answer
- Null lives only in object references on the JVM
- Primitive-backed value class + nullable = box
- Reference-backed value class + nullable = often no box
- Other boxing triggers still apply regardless
- Rule recurses to the eventual JVM representation
basics
~20 sA non-null value uses the raw underlying value directly. A nullable value needs to represent 'no value', which a primitive can't, so Kotlin wraps it in an object. If the underlying type is already an object, nullability is cheaper.
solid answer
~40 sFor `Color(val rgb: Int)`, the unboxed representation is a primitive `int`. A primitive `int` has no way to encode `null`, so the moment the type becomes `Color?` the compiler must use the boxed wrapper class (a real object reference whose absence is `null`). Hence `Color?` always boxes. When the underlying type is itself a reference type — e.g. `value class Name(val s: String)` — the unboxed form is already a `String` reference, which can hold `null`. Kotlin can therefore represent `Name?` as a (possibly-null) `String` WITHOUT boxing in many cases, because the reference already carries nullability. The key insight: boxing on nullability is driven by whether the underlying type can natively represent null. Primitive-backed value classes box when nullable; reference-backed ones often don't.
code
kotlin · 8 lines@JvmInline value class Color(val rgb: Int) // primitive-backed
@JvmInline value class Name(val s: String) // reference-backed
fun c(x: Color): Int = x.rgb // unboxed int
fun cN(x: Color?): Int = x?.rgb ?: 0 // x is BOXED (Int can't be null)
fun n(x: Name): String = x.s // unboxed String ref
fun nN(x: Name?): String = x?.s ?: "" // x stays a nullable String ref, no boxgo deeper
Knows nullable value classes can box but may not explain the primitive-vs-reference distinction.
Explains that null requires an object reference, so primitive-backed nullable value classes box while reference-backed ones often don't.
Reasons about the JVM primitive/reference split, recurses the rule to the eventual representation, and notes other triggers still apply.
Uses this to make API/allocation trade-offs (choose underlying type, sentinel values) on hot paths and library boundaries.
## The core rule Whether `T?` boxes depends on whether the **underlying (unboxed) representation** can natively hold `null`. ### Primitive-backed value class ```kotlin @JvmInline value class Color(val rgb: Int) ``` - Unboxed form = JVM primitive `int`. - A primitive `int` cannot be `null`; every bit pattern is a valid color. - So `Color?` cannot be a bare `int`. The compiler falls back to the **boxed `Color`** object, whose `null` reference means absent. - Therefore: `Color` (non-null) → unboxed `int`; `Color?` → **boxed**. ### Reference-backed value class ```kotlin @JvmInline value class Name(val s: String) ``` - Unboxed form = `String` reference. - A reference can already be `null`. - So `Name?` can stay as a (nullable) `String` reference — **no boxing needed** for nullability alone. ## Why null forces boxing for primitives The JVM has two worlds: primitives (`int`, `long`, `double`, `boolean`, …) which are not references and cannot be `null`, and object references which can. Kotlin nullability (`?`) maps onto object references. So a nullable value whose unboxed form is a primitive has nowhere to put `null` except a real object. ```kotlin fun pick(c: Color?): Color = c ?: Color(0) // 'c' arrives BOXED because Color is Int-backed and nullable fun pickName(n: Name?): Name = n ?: Name("anon") // 'n' is just a nullable String reference — no box ``` ## Caveats - Even reference-backed value classes can still box for the **other** reasons: generics, `Any`/supertype, collections. Nullability is only one trigger. - A value class can also wrap another **unsigned/value** type; the rule recurses to the eventual JVM representation. ## Practical takeaway If you need cheap nullability and care about allocations on a hot path, prefer a reference-backed underlying type, or avoid the `?` by using a sentinel non-null value.
- Does `Name?` (String-backed) ever still box?Yes — if used as a generic argument, as Any/supertype, or stored in a collection. Nullability just isn't the trigger here.
- What about `value class Flag(val b: Boolean)` as Flag??Boolean is a primitive, so Flag? boxes, just like the Int-backed case.
A primitive is a light switch — always on or off, never 'unplugged'. To say 'maybe no switch at all' you need a whole box that can be missing.
saying these in an interview costs you the question
- Saying all value classes box when nullable, ignoring reference-backed underlying types
- Claiming primitives can hold null with some flag
- Forgetting that generics/Any still box reference-backed nullable value classes
- Confusing nullability boxing with autoboxing of java.lang.Integer only