skip to content

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%

answer

  1. Inlined wrapper vanishes → descriptors can collide
  2. Compiler adds a hash suffix with a hyphen
  3. Hyphen = not a legal Java identifier (intentional)
  4. @JvmName restores a Java-callable name
  5. Return type alone can trigger mangling; boxed-only params don't

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.

solid answer

~40 s

When a value class is inlined, a function `fun f(c: Color)` compiles to JVM bytecode that actually takes an `int`, i.e. `f(I)`. But `fun f(i: Int)` ALSO compiles to `f(I)`. Those collide. To keep them separate, the compiler appends a deterministic hash suffix to the value-class-using function's JVM name, producing something like `f-aBcDeF(I)`. This name mangling is why such functions are effectively uncallable from Java without `@JvmName`, and why you may see strange names in stack traces/reflection. Adding `@JvmName("...")` gives a stable name. Constructors and the unbox accessor are similarly affected. Importantly, mangling also means you can't accidentally produce a binary-compatible clash, but it does break naive Java callers and can be a source of confusion in mixed codebases.

code

kotlin · 9 lines
kotlin
@JvmInline value class Color(val rgb: Int)

// Both legal in Kotlin; would collide as paint(I)V on the JVM.
fun paint(c: Color) {}   // becomes paint-<hash>(I)V
fun paint(i: Int)   {}   // stays paint(I)V

// Make it callable from Java:
@JvmName("paintColor")
fun paintForJava(c: Color) {}   // exported as paintColor(int)

go deeper

for a junior

May recognize odd names in stack traces but can't explain the collision cause.

for a middle

Explains that inlining erases the wrapper so descriptors collide, hence a hash suffix is added.

for a senior

Covers the hyphen-as-non-Java-identifier rationale, @JvmName remedy, return-type triggering, and boxed-param exemption.

for a principal

Discusses binary-compatibility implications of Int↔value-class swaps and weighs Java-interop ergonomics when exposing value classes in public APIs.

## The signature-collision problem Because an inlined value class is represented by its underlying type at the JVM level, the wrapper vanishes from the descriptor: ```kotlin @JvmInline value class Color(val rgb: Int) fun paint(c: Color) {} // JVM descriptor: (I)V — takes an int fun paint(i: Int) {} // JVM descriptor: (I)V — also an int! ``` Both would be `paint(I)V` on the JVM — an illegal duplicate. Kotlin allows both at source level, so it must disambiguate at the bytecode level. ## The fix: name mangling The compiler appends a short, deterministic **hash suffix** (derived from the parameter types) to the JVM method name of the function that uses a value class: ``` paint-<hash>(I)V // the Color overload paint(I)V // the Int overload ``` The hyphen makes the name **not a legal Java identifier**, which is intentional: Java cannot call it by accident, and Kotlin call sites resolve it transparently. You'll see these mangled names in: - stack traces, - `javap` output, - reflection (`Method.getName()`), - error messages. ## When mangling happens - A function **takes** a value-class parameter (in the unboxed position), or - a function **returns** a value class (the return type alone can trigger it), - including constructors and property accessors generated for value classes (`box-impl`, `unbox-impl`, `constructor-impl`). - A function that only uses the **boxed** form (e.g. `Color?`) is NOT mangled, because the descriptor already references the wrapper type and can't collide. ## Java interop consequence From Java you normally cannot call a mangled function: ```kotlin @JvmName("paintColor") fun paint(c: Color) {} ``` Applying `@JvmName` gives a **stable, Java-callable name** (it overrides the mangled name). Without it, Java sees only `paint-xxxx`, which it can't reference. ## Why not just forbid the overloads? Mangling lets Kotlin keep value classes a true zero-cost abstraction while preserving Kotlin's overload semantics, at the price of degraded Java ergonomics. It also future-proofs binary compatibility: changing a parameter from `Int` to `Color` changes the mangled name, so it's a **binary-incompatible** change even though source looks similar. ## Takeaways - Mangling is the compiler's collision-avoidance for inlined value classes. - Hyphenated names are deliberately Java-hostile; use `@JvmName` for interop. - Returning a value class can mangle too, not just parameters. - Changing Int↔value-class params is a binary break.

  • Does a function that only takes `Color?` get mangled?
    No. The nullable form is boxed, so the descriptor already references the wrapper type and can't collide with the Int overload.
  • How do you make a value-class function callable from Java?
    Annotate it with @JvmName("stableName") to override the mangled name with a plain Java identifier.
  • Is switching a parameter from Int to a value class a binary-compatible change?
    No. It changes the mangled JVM name, so existing compiled callers break.

Two people named 'Sam' in one room cause confusion, so you tag one 'Sam#7f3'. The tag keeps them apart but is awkward to shout across the room (Java).

saying these in an interview costs you the question

  • Claiming Kotlin simply forbids the Int/value-class overload pair
  • Saying mangled functions are freely callable from Java
  • Believing only parameters, never return types, trigger mangling
  • Not knowing @JvmName fixes interop
  • Thinking the hyphen is a bug rather than intentional

context