skip to content

Show how two Kotlin functions can produce a JVM signature clash due to type erasure, and how @JvmName resolves it.

level: middleimportance: must knowfreq 48%

answer

  1. Generics erase: List<String> and List<Int> -> List
  2. Same descriptor = 'platform declaration clash'
  3. @JvmName splits the bytecode names
  4. Kotlin still sees overloads; Java needs both names
  5. Common with extension-receiver overloads

basics

~20 s

Generics disappear at runtime, so List<String> and List<Int> both become plain List on the JVM. Two functions that differ only by that generic part get the same JVM signature and won't compile. Adding @JvmName to one gives it a different bytecode name, so both compile.

solid answer

~40 s

On the JVM, generic type arguments are erased: `List<String>` and `List<Int>` both erase to `List`. If two Kotlin functions share a name and differ only in an erased generic parameter (receiver or argument), they compile to identical JVM descriptors (e.g. both `process(Ljava/util/List;)V`), which the JVM forbids — Kotlin reports a 'platform declaration clash' / 'accidental override' error. `@JvmName` on at least one of them emits a distinct method name in bytecode, eliminating the descriptor collision. Kotlin source still sees overloaded names because Kotlin overload resolution uses the full (non-erased) generic signatures; only the bytecode names diverge. This is the canonical reason @JvmName exists for member and extension functions whose Kotlin overloads are erasure-equivalent.

code

kotlin · 8 lines
kotlin
fun List<String>.flatten2(): String = joinToString()

@JvmName("flattenInts")
fun List<Int>.flatten2(): Int = sum()

// Without @JvmName: error "Platform declaration clash: ... have the same JVM signature (flatten2(Ljava/util/List;))"
// Kotlin call sites: listOfStrings.flatten2(); listOfInts.flatten2()
// Java call sites:   flatten2(stringList); flattenInts(intList);

go deeper

for a junior

Recognizes that generics erase and that @JvmName can fix a name conflict, even if hazy on the descriptor detail.

for a middle

Constructs a concrete erasure-clash example and applies @JvmName correctly, naming the 'platform declaration clash' error.

for a senior

Explains descriptors, why Kotlin overload resolution still works, and when to prefer different Kotlin names over @JvmName.

for a principal

Weighs API ergonomics across Kotlin and Java consumers and the long-term cost of dual names; reasons about reified vs. erased generics in API design.

## The problem: type erasure **Type erasure** means the JVM does not keep generic type arguments at runtime. A function taking `List<String>` and another taking `List<Int>` both have the **erased** parameter type `java.util.List`. The JVM identifies a method by its **descriptor**: name + erased parameter types + erased return type. If two methods produce the same descriptor in the same class, the class file is invalid. ## Where Kotlin lets you write the clash Kotlin's overload resolution operates on full generic signatures, so the source below looks fine to a Kotlin reader, but it cannot be emitted: ```kotlin // COMPILE ERROR: "Platform declaration clash" fun process(items: List<String>) { /* ... */ } fun process(items: List<Int>) { /* ... */ } // Both erase to: process(Ljava/util/List;)V ``` The same happens for **extension functions** that differ only by an erased generic receiver: ```kotlin fun List<String>.sum2(): String = joinToString() fun List<Int>.sum2(): Int = sum() // clash: both are sum2(List) ``` ## The fix: @JvmName Annotate one (or both) of the clashing functions to give it a different **bytecode** name: ```kotlin fun process(items: List<String>) { /* ... */ } @JvmName("processInts") fun process(items: List<Int>) { /* ... */ } ``` Now the descriptors are `process(Ljava/util/List;)V` and `processInts(Ljava/util/List;)V` — distinct, valid. **Kotlin callers** keep calling `process(...)` for both (overload resolution picks by full type). **Java callers** must use `process` for the String version and `processInts` for the Int version. ## Important nuances - This only works because the clash is a **naming** collision, not a semantic one — @JvmName separates the bytecode names. - For **inline functions with reified type parameters**, you often don't hit the clash because reified generics are resolved at the call site, but for ordinary (non-reified) generics the erasure clash is real. - An alternative to @JvmName is simply giving the functions different Kotlin names; @JvmName is preferred when you want a uniform, idiomatic Kotlin name (`process`) while still emitting valid bytecode. ## Summary Erasure makes generic-only-different overloads collide on the JVM; `@JvmName` resolves it by decoupling the Kotlin name from the emitted bytecode name.

  • Why does Kotlin still let you call both overloads by the same name from Kotlin?
    Kotlin overload resolution uses the full, non-erased generic signatures, so it can distinguish List<String> from List<Int>; only the emitted JVM names differ.
  • What exact error does the compiler give without @JvmName?
    A 'Platform declaration clash' (a.k.a. 'accidental override') error stating the declarations have the same JVM signature.

saying these in an interview costs you the question

  • Saying generics are retained at runtime on the JVM
  • Claiming Kotlin can't even compile the @JvmName-fixed version
  • Thinking @JvmName changes the parameter types or descriptor beyond the name
  • Confusing the fix with @JvmStatic
  • Believing reified generics never relate to erasure

context