skip to content

Can default arguments or @JvmOverloads cause platform declaration clashes? How do you reason about the generated signatures?

level: seniorimportance: should knowfreq 28%

answer

  1. Defaults alone -> one method + $default synthetic
  2. @JvmOverloads -> telescoping Java overloads
  3. Expand, then erase, then compare descriptors
  4. Generated overload can clash with a manual one
  5. Works on constructors too

basics

~20 s

Yes. @JvmOverloads expands a function with default values into several Java overloads. If those generated overloads, after erasure, match an existing function's signature, you get a clash. You fix it by renaming with @JvmName or removing the overlap.

solid answer

~40 s

Kotlin default arguments do not create overloads by themselves — they compile to one method plus a synthetic `$default` helper. But `@JvmOverloads` instructs the compiler to emit one Java-visible overload per trailing default parameter. Each generated overload has its own JVM descriptor, and any of them can collide — after erasure — with a manually written function or with another `@JvmOverloads` expansion. For example, `fun f(a: Int, b: List<String> = emptyList())` with `@JvmOverloads` emits `f(int)` and `f(int, List)`; if a separate `fun f(a: Int, b: List<Int>)` exists, its erased `f(int, List)` clashes with the generated one. To reason about it, expand `@JvmOverloads` mentally, erase generics, and compare descriptors. Fix via `@JvmName`, distinct Kotlin names, or dropping `@JvmOverloads` where an explicit overload already covers the case.

code

kotlin · 10 lines
kotlin
// Clash: generated load(int, List) vs erased load(int, List)
@JvmOverloads
fun load(id: Int, tags: List<String> = emptyList()) {}
fun load(id: Int, scores: List<Int>) {}

// Fix
@JvmOverloads
fun load(id: Int, tags: List<String> = emptyList()) {}
@JvmName("loadScores")
fun loadScores(id: Int, scores: List<Int>) {}

go deeper

for a junior

Knows defaults exist but may not connect them to clashes.

for a middle

Understands @JvmOverloads creates multiple overloads and that defaults alone use a $default helper.

for a senior

Predicts clashes by expanding overloads and erasing generics, and picks the right fix.

for a principal

Designs APIs to avoid telescoping/erasure collisions and decides where @JvmOverloads belongs in the public contract.

## Default arguments alone: usually no clash A function with defaults compiles to a **single** real method plus a synthetic bridge: ```kotlin fun greet(name: String, loud: Boolean = false) { } // JVM: greet(String, boolean) + greet$default(String, boolean, int, Object) ``` The `$default` synthetic carries a bitmask for which args were omitted. No extra public overloads — so no clash by itself. ## @JvmOverloads expands the surface `@JvmOverloads` makes Kotlin emit a telescoping set of Java overloads, one per trailing default: ```kotlin @JvmOverloads fun greet(name: String, loud: Boolean = false) { } // JVM: greet(String) AND greet(String, boolean) ``` Each generated overload is a real JVM method with its own descriptor. **Any** of them can collide. ## How erasure turns this into a clash ```kotlin @JvmOverloads fun load(id: Int, tags: List<String> = emptyList()) { } // generates load(int) and load(int, List) fun load(id: Int, scores: List<Int>) { } // erases to load(int, List) -> CLASHES with the generated load(int, List) ``` The generic argument is erased, so both two-arg forms become `load(int, java.util.List)`. ## A reasoning recipe 1. **Expand** every `@JvmOverloads` into its full overload set. 2. **Erase** all generic type arguments to raw types. 3. **Box/unbox is irrelevant** — primitives stay primitive in descriptors. 4. **Compare descriptors**: name + erased params. Any duplicate is a clash. ## Fixes - `@JvmName` on the clashing declaration(s). - Give one function a distinct **Kotlin** name. - Drop `@JvmOverloads` if an explicit overload already provides the Java entry point. - Reorder/restructure parameters so the erased descriptors differ. ## Subtlety: constructors `@JvmOverloads` on a secondary/primary constructor expands constructors the same way; the same erasure-collision reasoning applies to `<init>` descriptors.

  • Do plain default arguments (no @JvmOverloads) generate extra public overloads that could clash?
    No. They compile to a single method plus a synthetic $default helper, so they don't add public overloads. Clashes from defaults appear only once @JvmOverloads expands them.
  • How do you mentally detect a clash before compiling?
    Expand all @JvmOverloads, erase every generic argument to its raw type, then compare each method's name + erased parameter list. Duplicates are clashes.

saying these in an interview costs you the question

  • Believing default arguments always create Java overloads
  • Forgetting to expand @JvmOverloads before comparing signatures
  • Ignoring erasure when comparing the generated overloads
  • Assuming constructors are immune to the same expansion

context