skip to content

What are the limitations and common pitfalls of @JvmOverloads — when does it not apply or cause clashes?

level: seniorimportance: should knowfreq 35%

answer

  1. No default = no-op
  2. Right-to-left only, no middle skip
  3. Platform declaration clash with manual overloads
  4. Not on abstract/interface bodies
  5. Enlarges binary API surface

basics

~20 s

It only helps with trailing defaults dropped right-to-left, needs at least one defaulted parameter to do anything, doesn't work on abstract/interface methods, and can clash with hand-written overloads. It also enlarges your public API surface.

solid answer

~40 s

Pitfalls: (1) No defaulted parameter means nothing is generated — the annotation is a no-op. (2) Defaults drop only right-to-left, so you can't get a middle-skip overload. (3) It cannot be used on abstract members or interface methods that have no body (no defaults allowed there), nor does it make sense on members that can't carry defaults. (4) Generated overloads can collide with manually declared overloads of the same JVM signature, causing a 'platform declaration clash' or 'accidental override' compile error. (5) It bloats the binary API surface and can complicate API evolution — adding a parameter shifts the ladder. (6) On open/override methods, beware signature conflicts in subclasses. (7) It's irrelevant if no Java/JVM consumer exists. Prefer it deliberately at interop boundaries, not blanket-applied.

code

kotlin · 4 lines
kotlin
// Clash risk: generated overload duplicates a manual one
@JvmOverloads
fun send(msg: String, retries: Int = 3) {}
fun send(msg: String) {}   // ERROR: conflicting overloads / platform declaration clash

go deeper

for a junior

Knows it needs a defaulted parameter to do anything.

for a middle

Lists right-to-left limitation and that it's interop-only.

for a senior

Diagnoses platform declaration clashes and explains API-surface costs.

for a principal

Treats it as a binary-compatibility/public-API governance decision and weighs builders/explicit overloads as alternatives.

## When it does nothing @JvmOverloads only matters when there is **at least one parameter with a default value**. On a function with no defaults it generates no extra methods — applying it is harmless but pointless. ## Right-to-left only It strips trailing defaults; you cannot obtain an overload that supplies a later parameter while omitting an earlier defaulted one. If callers need that flexibility, write explicit overloads or expose a builder. ## Where it can't be applied / makes no sense - **Abstract methods / interface methods without a body** can't have default parameter values to expand, so there's nothing to generate. - It's a JVM-target annotation, so it's meaningless in pure non-JVM contexts (Kotlin/Native, JS) — relevant only for JVM/Java interop. ## Signature clashes The biggest real-world trap: a **generated overload colliding with a hand-written one**. Example: ```kotlin fun log(msg: String, level: Int = 0) {} fun log(msg: String) {} // hand-written @JvmOverloads fun log2(msg: String, level: Int = 0) {} // generates log2(String) — fine here, but if a sibling // fun log2(String) existed it would clash ``` If the generated `log2(String)` duplicates an existing `log2(String)`, the compiler reports a **platform declaration clash** ("conflicting overloads" / accidental override). The same can happen across inheritance when an override plus generated overloads produce duplicate erased signatures. ## API-surface and evolution costs - Each defaulted parameter multiplies the **public method count** other JVM code can bind to. Removing or reordering parameters later is a **binary-incompatible** change for those overloads. - For libraries, this makes @JvmOverloads a deliberate **public-API commitment**, not a free convenience. ## Visibility & modifiers - The generated overloads inherit the function's visibility; annotating a `private` function gains nothing useful for Java. - Works on top-level functions, members, and constructors; placement on a primary constructor needs the explicit `constructor` keyword. ## Guidance Apply it **only where a real Java/JVM caller benefits** at an interop boundary. Don't sprinkle it across an all-Kotlin codebase — it adds surface area with no benefit. When middle-skipping or complex defaulting is needed, prefer explicit overloads, builders, or named arguments (Kotlin-only).

  • Why might adding @JvmOverloads cause a 'platform declaration clash' compile error?
    A generated reduced-arity overload can have the same JVM erased signature as a manually declared overload, producing conflicting/duplicate declarations.
  • Is @JvmOverloads useful in an all-Kotlin module with no Java consumers?
    No real benefit — Kotlin already uses defaults directly; it just enlarges the JVM method surface for nothing.

saying these in an interview costs you the question

  • Believing it can generate middle-skipping overloads
  • Not recognizing clash errors with manual overloads
  • Applying it blanket-wide with no interop need
  • Ignoring its public-API / binary-compatibility implications
  • Thinking it works on bodyless abstract/interface members

context