skip to content

Boxing & Unboxing Rules

The underlying value is inlined most of the time, but nullable uses, generic positions, collections, and anything typed as a supertype force boxing. The other detail interviewers like is the mangled JVM method names, which is why value classes complicate Java interop.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What does it mean for a Kotlin @JvmInline value class to be "boxed," and when does the compiler keep it inlined (unboxed) instead?

level: juniorimportance: must knowfreq 55%

answer

  1. Unboxed = wrapper erased, raw value passed directly
  2. Boxed = real object on heap
  3. Boxes on: nullable, generics, supertype/Any, collections
  4. Stays unboxed when static type is exactly the value class, non-null
  5. Boxing is correct but costs an allocation

basics

~20 s

Normally 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 s

A @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
kotlin
@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

for a junior

Can state the unboxed-by-default idea and name at least nullable and collections as boxing triggers.

for a middle

Lists all four trigger categories (nullable, generics, supertype/Any, collections) and explains the static-type rule.

for a senior

Ties boxing back to the performance intent and reasons about keeping hot paths unboxed.

for a principal

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

context

open as a page

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.

level: middleimportance: should knowfreq 45%

basics

~20 s

A 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.

open as a page

You have `@JvmInline value class Meters(val v: Double) : Comparable<Meters>`. Walk through which of these box: `meters.compareTo(other)`, `listOf(a, b).sorted()`, and passing `meters` to `fun <T> id(x: T): T`. How can boxing reintroduce the allocation cost the value class was meant to avoid?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Calling its own methods directly stays cheap. But the moment values flow through generic code or an interface (like sorting a list), Kotlin wraps them into objects, so you pay allocations exactly where you hoped to save them.

open as a page

Why does the Kotlin compiler mangle function names that take or return @JvmInline value class parameters on the JVM, and what practical consequences does this have for overloads and Java interop?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Because the wrapper disappears, two different Kotlin functions could end up with identical JVM signatures and clash. Kotlin renames (mangles) the methods with a hash suffix to keep them distinct, which makes them awkward to call from Java.

open as a page

When designing a public Kotlin library API, what boxing- and mangling-related trade-offs should drive whether you expose a @JvmInline value class versus a plain type or a data class — especially considering Java consumers and identity-sensitive operations like equals/hashCode and `===`?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Value classes give type safety with no allocation in the common case, but they box at many boundaries and produce ugly, Java-unfriendly names. Weigh those costs against the safety benefit, especially for Java users and code that compares object identity.

open as a page