How does erasure of inline value classes interact with signature clashes, and when does @JvmName help versus the automatic name mangling Kotlin already applies?
answer
- value class erases to underlying type
- Compiler auto-mangles with -hash suffix
- Unsigned types are value classes too
- Mangled name is not Java-callable
- @JvmName pins a clean name but you own clashes
basics
~20 sAn inline value class is erased to its underlying type on the JVM, so two functions taking different value classes over the same base type would clash. Kotlin auto-mangles their names to avoid that; @JvmName is for the generic-collection case it can't auto-fix.
solid answer
~50 sA `value class` (formerly `inline class`) wrapping, say, a `String` is erased at the boundary to `String`. So `fun f(x: UserId)` and `fun f(x: OrderId)`, where both wrap `String`, would both become `f(String)` — a clash. Kotlin prevents this automatically by **name mangling**: it appends a stable hash suffix (e.g. `f-abc123`) to functions that expose a value-class parameter, so each gets a unique JVM name without you doing anything. That mangling also makes such functions hard to call cleanly from Java, which is why you sometimes add `@JvmName` to give a stable, Java-friendly name (accepting that you must then ensure no real clash). For generic-collection erasure (`List<Int>` vs `List<String>`) there is no auto-mangling — Kotlin reports the clash and you resolve it with `@JvmName`. So: value-class clashes are auto-mangled; generic-erasure clashes are your responsibility via `@JvmName`.
code
kotlin · 9 lines@JvmInline value class Meters(val v: Double)
@JvmInline value class Feet(val v: Double)
// Both erase to (double); compiler mangles names so no clash.
fun area(side: Meters): Double = side.v * side.v
fun area(side: Feet): Double = side.v * side.v
// Pin a Java-friendly name (now YOU must keep names unique):
@JvmName("areaMeters") fun areaM(side: Meters): Double = side.vgo deeper
Knows value classes have an underlying type but may not connect to mangling.
Understands erasure to underlying type and that the compiler avoids clashes automatically.
Distinguishes auto-mangling (value classes) from manual @JvmName (generic collections) and the Java-interop tradeoff.
Designs value-class APIs for Java consumers, deciding where to pin @JvmName and where to expose hand-written overloads.
## Value classes and erasure A `@JvmInline value class UserId(val raw: String)` has **no wrapper object** at runtime in most positions — it is erased to its underlying `String`. That keeps it allocation-free but means the type information is gone at the JVM boundary. ```kotlin @JvmInline value class UserId(val raw: String) @JvmInline value class OrderId(val raw: String) fun lookup(id: UserId) {} // would be lookup(String) fun lookup(id: OrderId) {} // would also be lookup(String) ``` Without intervention these two would clash exactly like `List<Int>` vs `List<String>`. ## Kotlin's automatic name mangling To avoid the clash, the compiler **mangles** the JVM names of functions that take (or return) a value-class type, appending a `-<hash>` suffix derived from the signature, e.g. `lookup-PdyMfdE`. Because the suffix depends on the value-class types involved, the two `lookup` functions get **different** mangled names — no clash, no annotation required. This is automatic and applies to value-class boundaries (and `Result`-like unsigned types `UInt`/`ULong`/`UByte`/`UShort`, which are themselves value classes). ## Why mangling complicates Java interop The mangled name (`lookup-PdyMfdE`) is not a legal Java identifier you can type, so Java cannot call it directly. Two responses: - Add `@JvmName("lookupUser")` to pin a clean Java name — but now **you** are responsible: if a second function ends up with the same erased descriptor and the same forced name, you reintroduce a clash and must rename it too. - Keep value-class functions `internal`/Kotlin-only, exposing a hand-written Java-facing overload. ## Contrast with generic-collection clashes Kotlin does **not** auto-mangle generic-collection overloads — `f(List<Int>)` vs `f(List<String>)` produce a hard *platform declaration clash* that the compiler refuses, and you fix it manually with `@JvmName`. The difference: value classes carry enough static type identity for the compiler to synthesize unique names; raw generic containers do not, so the compiler defers to you. ## Decision summary - **Two value classes over the same base type** -> auto-mangled, compiles; add `@JvmName` only for Java ergonomics. - **Generic-collection overloads** -> compiler error; you must add `@JvmName` or distinct names. - Adding `@JvmName` over mangling means you own clash-avoidance for those names.
- Why can't Java easily call a function that takes a value-class parameter?Its JVM name is mangled to something like area-PdyMfdE, which is not a valid Java identifier, so it cannot be referenced from Java source without @JvmName.
- Are UInt/ULong subject to the same mangling?Yes. Unsigned types are value classes erased to their signed counterparts, so functions exposing them are name-mangled just like custom value classes.
Mangling is like the system auto-appending a serial number to duplicate filenames; @JvmName is you manually renaming the file and promising no two collide.
saying these in an interview costs you the question
- Saying value-class overloads cause a hard clash like generic collections (they auto-mangle)
- Not knowing value classes are erased to the underlying type
- Forgetting @JvmName transfers clash-avoidance responsibility to you
- Unaware unsigned integer types are value classes