What does it mean for a Kotlin @JvmInline value class to be "boxed," and when does the compiler keep it inlined (unboxed) instead?
answer
- Unboxed = wrapper erased, raw value passed directly
- Boxed = real object on heap
- Boxes on: nullable, generics, supertype/Any, collections
- Stays unboxed when static type is exactly the value class, non-null
- Boxing is correct but costs an allocation
basics
~20 sNormally the value class disappears and only its single wrapped value is used directly, which is fast. Sometimes Kotlin must wrap it back into a real object (boxing), like when it is null or stored generically.
solid answer
~40 sA @JvmInline value class wraps one property. In the common case the compiler erases the wrapper and passes the underlying value directly (inlined/unboxed), so there is no extra allocation. Boxing means the compiler creates an actual instance of the wrapper class on the heap. This happens when the value must be treated as an object reference: used as a nullable type, used through a generic type parameter, stored in a collection like List<T>, or used as Any or a supertype/interface it implements. As a rule of thumb: a value class stays unboxed when its static type is exactly the value class and non-null; it boxes whenever it has to flow through a slot that expects an object. Boxing is correctness-preserving but reintroduces the allocation the class was meant to avoid.
code
kotlin · 11 lines@JvmInline
value class UserId(val raw: Long)
fun direct(id: UserId): Long = id.raw // unboxed: param is a long
fun demo() {
val id = UserId(7) // unboxed
val nullable: UserId? = id // boxed (nullable)
val anyRef: Any = id // boxed (Any)
val ids: List<UserId> = listOf(id) // boxed (generic List)
}go deeper
Can state the unboxed-by-default idea and name at least nullable and collections as boxing triggers.
Lists all four trigger categories (nullable, generics, supertype/Any, collections) and explains the static-type rule.
Ties boxing back to the performance intent and reasons about keeping hot paths unboxed.
Frames value classes as a zero-cost abstraction whose guarantee leaks at object-reference boundaries, and weighs that when designing public APIs.
## What a value class is A `@JvmInline value class` wraps exactly one read-only property: ```kotlin @JvmInline value class UserId(val raw: Long) ``` The point is zero-overhead typing: at runtime, in the good case, there is no `UserId` object — the JVM just sees a `long`. ## Inlined (unboxed) vs boxed - **Inlined / unboxed**: the wrapper is erased and the underlying value (`raw`) is used directly. No heap allocation. This is the default the compiler tries hard to keep. - **Boxed**: the compiler allocates a real instance of the generated class (it has a `box-impl`/`unbox-impl` pair under the hood) so the value can be handled as an object reference. ## When boxing happens The value must be represented as an `Object` reference, so it boxes when: - **Nullable use** — `UserId?`. `null` can't be a primitive `long`, so the type becomes the boxed class. - **Generics** — used as a type argument, e.g. `List<UserId>`, `Pair<UserId, UserId>`, or any generic function parameter `T`. Generic slots erase to `Object`. - **Supertype / interface / Any** — assigned to `Any` or to an interface the value class implements; that requires an object reference. - **Collections / arrays-of-Object** — `listOf(userId)` boxes each element. (A dedicated `Array<UserId>` is also boxed; only the primitive `LongArray` etc. would be unboxed but that loses the type.) ## When it stays unboxed When the **static type is exactly the value class and non-null** and it's used directly — local variables, parameters typed `UserId`, return type `UserId`. Example: ```kotlin fun create(): UserId = UserId(42) // returns a long, unboxed fun length(id: UserId): Int = ... // takes a long, unboxed val id: UserId = create() // stays unboxed val maybe: UserId? = id // BOXED (nullable) val list = listOf(id) // BOXED (generic) ``` ## Why it matters The entire value of an inline value class is avoiding allocation while keeping a distinct type. Knowing what triggers boxing lets you keep hot paths allocation-free.
- Does putting a value class into a List<UserId> box it?Yes. List is generic, its element slot erases to Object, so each element is boxed.
- Is a non-null UserId local variable boxed?No. Its static type is exactly UserId and non-null, so it stays as the underlying value (unboxed).
Like shipping a single book: usually you hand it over bare (unboxed); but if it must go into a mixed parcel of 'any item' or be marked 'maybe empty', you put it in a box first.
saying these in an interview costs you the question
- Claiming value classes are 'never' allocated under any circumstance
- Thinking nullable UserId? stays unboxed
- Confusing value class with data class (data class always allocates)
- Assuming listOf(id) keeps it inlined