skip to content

What are the hard constraints on a @JvmInline value class declaration, and what interop/design consequences follow from name mangling?

level: principalimportance: nice to knowfreq 20%

answer

  1. one val, no var/second field/lateinit/inner
  2. interfaces yes, extending class no
  3. functions get mangled JVM names
  4. Java interop needs @JvmName wrappers
  5. boxes when nullable/generic/Any => not always free

basics

~20 s

A value class must have exactly one immutable property, can't store extra state, can't extend a class, and can't be inner. Because Kotlin renames its functions, calling them from Java is awkward and you usually need @JvmName.

solid answer

~50 s

Hard declaration constraints: exactly one `val` in the primary constructor (no `var`, no second stored property, no `lateinit`); the class may have computed `val`s, functions, `init`, and implement interfaces, but cannot extend a class, be `inner`, or be `data`+`value` combined meaningfully for multi-field needs; the underlying type can't make it recursive without a finite representation. Design/interop consequences come mainly from **name mangling**: any function that takes or returns a value class gets a hashed JVM name, so Java callers can't invoke it by its Kotlin name and must rely on `@JvmName`-exposed wrappers — pushing teams to keep value classes out of public Java-facing APIs or provide adapter layers. Also, value classes **box** when used as nullable, generic type arguments, captured as `Any`/interface types, or stored in collections, so the zero-overhead promise holds only on the unboxed path. Equality/hashCode are value-based, which interacts correctly across boxing. These constraints shape whether a value class is the right abstraction for a given boundary.

go deeper

for a junior

Recalls the one-val rule and that @JvmInline is required.

for a middle

Lists the main declaration constraints (no var, single field, no extending a class).

for a senior

Explains name mangling, @JvmName interop fixes, and the boxing situations that erode zero overhead.

for a principal

Decides where value classes belong (internal vs boundary), trading mangling/boxing against type safety, and sets team conventions.

## Declaration constraints (what won't compile) For `@JvmInline value class X(val v: T)`: - **Exactly one** property, and it must be a `val` in the **primary constructor**. No `var`; no additional stored properties (extra members must be computed `val`s or functions with no backing field). - No `lateinit`, no `inner` modifier. - Cannot **extend a class** (implicitly `Any`); it **may implement interfaces**. - No `init`-introduced second field; the only state is the one constructor property. - The underlying type must have a finite representation (no unbounded recursive nesting). ```kotlin @JvmInline value class Id(val raw: Long) : Comparable<Id> { // OK: interface override fun compareTo(other: Id) = raw.compareTo(other.raw) } // value class Bad(var x: Int) // ERROR: var // value class Bad2(val a: Int, val b: Int) // ERROR: two properties ``` ## Name mangling and Java interop Because unboxing collapses `X` to `T` in signatures, Kotlin **mangles** the JVM name of every function that references a value class in its signature (a stable hash suffix). Consequences: - **Java callers** can't call such a function by its Kotlin name; they'd have to use the mangled name (impractical) or you expose a clean overload via `@JvmName`. - **Reflection / frameworks** that look up methods by name can be surprised by mangled names. - Best practice: keep value classes off **public Java-facing API boundaries**, or wrap them with `@JvmName`-annotated adapters. ```kotlin @JvmName("distance") fun distanceMeters(a: Meters, b: Meters): Meters = Meters(b.v - a.v) ``` ## Boxing breaks the zero-overhead promise The unboxed (allocation-free) form applies to plain typed usage. The wrapper is **boxed** (allocated) when the value class is: - a **nullable** type (`Meters?`), - used as a **generic type argument** (`List<Meters>`), - assigned to a **supertype** (`Any`, an implemented interface), - placed where a reference object is structurally required. So on hot paths avoid those usages if allocation matters; the type system still works, you just lose the perf benefit. ## Design framing (principal level) - Value classes are best for **internal domain primitives** where you control both sides (Kotlin↔Kotlin). - At interop or serialization boundaries, weigh mangling and boxing; sometimes a plain type plus validation, or a regular class, is clearer. - Equality is value-based and survives boxing, so semantics remain correct even when perf degrades — important when reasoning about correctness vs. performance separately.

  • Why is calling a value-class function from Java hard?
    Its JVM name is mangled with a hash suffix; Java can't use the Kotlin name. You expose a clean name via @JvmName or avoid value classes on the Java-facing boundary.
  • Name three situations where a value class boxes.
    As a nullable type, as a generic type argument, and when upcast to Any or an implemented interface.

saying these in an interview costs you the question

  • Claiming value classes never allocate, ignoring boxing
  • Saying they can extend another class
  • Not knowing name mangling affects Java interop
  • Allowing a second stored property or var
  • Recommending value classes freely across Java-facing public APIs

context