skip to content

When you expose a companion factory function to Java callers, what does the generated bytecode look like, and how do @JvmStatic and @JvmName change the call site?

level: principalimportance: nice to knowfreq 20%

answer

  1. default Java: Type.Companion.create(...)
  2. @JvmStatic emits real static → Type.create(...)
  3. @JvmName renames the generated JVM method
  4. @JvmName fixes generic signature clashes
  5. private ctor reached via synthetic accessor

basics

~10 s

By default Java must call the factory through a Companion field, like User.Companion.create(...). Adding @JvmStatic generates a real static method so Java can call User.create(...). @JvmName lets you rename the generated method.

solid answer

~40 s

A companion factory compiles to an instance method on a nested `Companion` class plus a `public static final Companion` field on the outer class. From Kotlin you call `User.create(...)`; from Java the default is `User.Companion.create(...)`. Annotating the companion function with `@JvmStatic` makes the compiler also emit a genuine `static` method directly on `User`, so Java can call `User.create(...)`. `@JvmName("...")` renames the generated JVM method — useful to avoid signature clashes (e.g. type-erased generics) or to give Java a different name. With a `private constructor`, the constructor is emitted with a synthetic accessor so the companion can reach it; Kotlin handles this automatically, but it's why a private constructor is still callable from the companion at the bytecode level. For clean Java interop on factories, prefer `@JvmStatic`; for renaming or clash avoidance, use `@JvmName`.

code

kotlin · 11 lines
kotlin
class User private constructor(val name: String) {
    companion object {
        @JvmStatic
        @JvmName("fromName")
        fun create(name: String): User = User(name)
    }
}

// Kotlin:  User.create("a")
// Java without @JvmStatic: User.Companion.create("a")
// Java with @JvmStatic+@JvmName: User.fromName("a")

go deeper

for a junior

Aware that Kotlin uses Type.create(...) and that Java interop differs.

for a middle

Knows Java defaults to Type.Companion.create(...) and that @JvmStatic enables Type.create(...).

for a senior

Explains the generated Companion field/class, when @JvmStatic vs @JvmName apply, and platform signature clashes.

for a principal

Designs the JVM-facing factory surface deliberately, managing binary compatibility, synthetic accessors for private constructors, and erasure-driven clashes.

## Default bytecode shape For: ```kotlin class User private constructor(val name: String) { companion object { fun create(name: String) = User(name) } } ``` the compiler generates roughly: - An outer class `User` with a private constructor and a `public static final User$Companion Companion` field. - A nested class `User$Companion` whose **instance** method `create(String)` calls the private constructor (via a synthetic accessor). So from **Kotlin**: `User.create("a")` (the `Companion` is implicit). From **Java**: `User.Companion.create("a")` — you go through the `Companion` field. ## @JvmStatic Annotating the companion function: ```kotlin companion object { @JvmStatic fun create(name: String) = User(name) } ``` makes the compiler **additionally** emit a real `static User create(String)` method on the `User` class itself (the Companion instance method still exists too). Now Java calls `User.create("a")` directly — idiomatic Java factory ergonomics. `@JvmStatic` only applies to members of `object`/`companion object`. ## @JvmName `@JvmName("fromName")` changes the **name of the generated JVM method** without changing the Kotlin name: ```kotlin @JvmStatic @JvmName("fromName") fun create(name: String) = User(name) ``` Use it to: - Avoid **platform signature clashes** — e.g. two functions that differ only by generic type parameters erase to the same JVM signature; `@JvmName` disambiguates them. - Provide a more Java-friendly name while keeping the Kotlin name idiomatic. ## Private constructor at the bytecode level A `private constructor` can't be called from a separate class directly, yet the `Companion` is a separate generated class. Kotlin emits a **synthetic accessor** (or marks the constructor accessible) so the companion can invoke it. This is automatic — it's the mechanism that lets the 'private constructor + companion factory' idiom work on the JVM. ## Practical guidance - Exposing factories to Java? Add **`@JvmStatic`** so call sites read `Type.create(...)`. - Hitting a 'platform declaration clash' on generic factories? Apply **`@JvmName`**. - Pure-Kotlin code needs neither; `Type.create(...)` already works. - Remember `@JvmStatic`/`@JvmName` affect only the **JVM surface**, not Kotlin call sites.

  • Does @JvmStatic remove the Companion instance method?
    No. The Companion class and its instance method still exist; @JvmStatic adds an extra real static method on the outer class as well.
  • Why might two companion factories need @JvmName?
    If they differ only by generic type arguments, type erasure makes their JVM signatures identical ('platform declaration clash'); @JvmName gives them distinct JVM names.

saying these in an interview costs you the question

  • Thinking Java can call Type.create(...) without @JvmStatic
  • Believing @JvmStatic changes the Kotlin call site
  • Assuming @JvmName affects Kotlin-visible names
  • Claiming a private constructor is uncallable even by its own companion
  • Confusing @JvmStatic (objects) with top-level functions' @JvmName-only file facade

context