skip to content

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%

answer

  1. Public API = contract, not just an optimization
  2. Mangling hurts Java; Int↔value-class swap is a binary break
  3. Zero-cost is conditional; boxes at boundaries
  4. === unreliable; boxes are fresh/uncached
  5. equals/hashCode structural over the wrapped value

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.

solid answer

~50 s

Exposing a `@JvmInline value class` in a public API is a contract decision, not just an optimization. Pros: distinct type safety with zero allocation when used directly (Kotlin callers, hot paths). Cons that drive the trade-off: (1) Mangled JVM names make functions awkward from Java unless you add `@JvmName`, and Int↔value-class param changes are binary-incompatible. (2) Boxing leaks back at every generic/nullable/`Any`/collection boundary, so the 'free' promise is conditional and easy for consumers to lose. (3) Identity: `===` on value classes compares the underlying value (there's no stable wrapper identity), and boxed instances aren't cached, so you must never rely on reference identity; `equals`/`hashCode` are structural over the wrapped value. For Java-heavy consumers or identity-sensitive APIs, a plain typealias-free wrapper or a small data class may be more predictable. Reserve value classes for Kotlin-first APIs where the type-safety-per-byte win is real.

code

kotlin · 14 lines
kotlin
@JvmInline value class UserId(val raw: Long)

fun a() {
    val x = UserId(1)
    val y = UserId(1)
    println(x == y)        // true: structural over raw
    val bx: Any = x
    val by: Any = y
    println(bx === by)     // NOT guaranteed: separate, uncached boxes
}

// Java-friendly export:
@JvmName("lookupById")
fun lookup(id: UserId): String = TODO()

go deeper

for a junior

Knows value classes save allocations but not the API-contract or identity nuances.

for a middle

Can compare value class vs data class on allocation and equality, and mentions Java friction.

for a senior

Weighs boxing leaks, mangling, and binary compatibility together when shaping an API.

for a principal

Sets a deliberate policy across Java interop, conditional zero-cost, identity semantics, and alternatives, choosing per consumer profile.

## The decision frame Choosing `@JvmInline value class` for a **public** API surface trades raw inlining for ergonomic and contractual costs. Evaluate along four axes. ### 1. Java interop & binary compatibility - Functions taking/returning the value class get **mangled** JVM names (hyphenated hash suffix), so Java can't call them without a `@JvmName` alias. - Swapping a parameter between `Int` and the value class changes the mangled name → **binary break** for existing compiled callers. - If a meaningful fraction of consumers are Java, the friction may outweigh the safety gain. ### 2. Conditional zero-cost The allocation-free benefit holds only for the concrete, non-null, non-generic path. It **leaks** at: - nullable use (`T?` for primitive-backed), - generics / collections (`List<T>`, `map`, `Comparator`), - `Any`/supertype/interface dispatch. Consumers can unknowingly box on every element, so don't market it as universally free. ### 3. Identity & equality semantics ```kotlin @JvmInline value class UserId(val raw: Long) ``` - `equals`/`hashCode` are **structural** over the wrapped value (auto-generated), like a data class. - `===` (referential equality) is unreliable: there's no guaranteed stable wrapper identity, and boxing produces **fresh, uncached** objects (unlike small `Integer` caching). Two boxes of the same value may be different references. - Therefore never key behavior on `===` or on a value class instance's identity. Locking on it, identity maps, etc. are bugs. ### 4. Alternatives - **Plain underlying type** (`Long`): zero ceremony, full Java ergonomics, but loses type safety (can mix up IDs). - **`typealias`**: documentation only, no safety, no boxing — but no real type. - **`data class`**: always a real object (allocation + identity), great Java story, structural equals; pay allocation everywhere but predictable. - **`@JvmInline value class`**: best per-byte safety for Kotlin-first hot paths; worst Java ergonomics. ## A pragmatic policy - Internal/Kotlin-first, allocation-sensitive → value class. - Public, Java-facing, or identity-sensitive → prefer data class or plain type, or provide `@JvmName` overloads and document boxing. - Never expose value classes through APIs that depend on reference identity. ## Bottom line Value classes are a sharp tool: a zero-cost typed wrapper whose guarantees and ergonomics degrade exactly at the boundaries public APIs live on. Design the surface deliberately.

  • Can you rely on `===` to dedupe value-class instances?
    No. Boxing creates fresh, uncached objects and there's no stable wrapper identity; use structural == over the wrapped value.
  • When would a data class be the better public choice?
    When Java consumers matter or the API needs predictable object identity/ergonomics, accepting the allocation cost everywhere.
  • How do you keep a value-class API usable from Java?
    Add @JvmName aliases (and document boxing), or expose plain-type overloads at the boundary.

A value class is a featherweight passport stamp — great for fast domestic travel, but every international border (Java/generics/null) makes you fill out the full form again.

saying these in an interview costs you the question

  • Promising value classes are universally allocation-free
  • Relying on === or instance identity for value classes
  • Ignoring Java-interop mangling in a public API
  • Assuming boxed value-class instances are cached like small Integers
  • Treating value class vs data class as purely a style choice

context