skip to content

How does the Kotlin/JVM target represent Kotlin features in bytecode so that Java callers can use them, and what annotations control this?

level: seniorimportance: should knowfreq 40%

answer

  1. top-level fns -> FooKt static methods
  2. properties -> getX/setX; @JvmField exposes the field
  3. @JvmStatic lifts companion/object members to real statics
  4. @JvmOverloads expands default params into Java overloads
  5. @JvmName / @file:JvmName rename facade or dodge signature clashes

basics

~10 s

Kotlin compiles to ordinary JVM bytecode, so Java can call it. Annotations like @JvmStatic, @JvmName, @JvmOverloads, and @JvmField adjust how Kotlin constructs appear to Java callers.

solid answer

~50 s

Because the JVM target emits standard JVM bytecode, Kotlin classes are plain Java classes under the hood — but Kotlin idioms map in specific ways. Top-level functions in `Foo.kt` become static methods on a synthetic `FooKt` class. Properties become getter/setter methods; `@JvmField` exposes the backing field directly instead. `object`/`companion object` members are instance methods on an `INSTANCE`/`Companion` field unless annotated `@JvmStatic`, which lifts them to real static methods. Functions with default parameter values generate a single method plus a synthetic `$default` bridge — add `@JvmOverloads` to generate true overloads Java can call. `@JvmName` renames the generated method/file (e.g., to dodge signature clashes or rename `FooKt`). The compiler also emits `@Metadata` so Kotlin tooling can reconstruct nullability and other info. These tools matter when a JVM-target library must present a clean API to Java consumers (Spring, frameworks).

code

kotlin · 10 lines
kotlin
class Counter {
    companion object {
        @JvmStatic fun zero(): Counter = Counter()  // Java: Counter.zero()
    }
    @JvmField val limit = 100                       // Java: counter.limit
}

@JvmOverloads
fun connect(host: String, port: Int = 8080, tls: Boolean = true) { /* ... */ }
// Java can call connect("h"), connect("h", 9090), connect("h", 9090, false)

go deeper

for a junior

Knows Kotlin compiles to bytecode that Java can call.

for a middle

Names the main interop annotations and the FooKt/getter-setter mappings.

for a senior

Explains companion/object mapping, default-param bridges, signature clashes, and designs a clean Java-facing API.

for a principal

Sets library API conventions (when to annotate) and weighs interop ergonomics vs idiomatic Kotlin across a published surface.

## Kotlin on the JVM is just bytecode The `jvm()` target compiles to standard **JVM bytecode**, so every Kotlin construct must be expressed in Java-visible terms. Understanding the mapping is essential when a JVM-target library is consumed from Java. ## Default mappings - **Top-level functions/properties** in `Util.kt` become **static members of a class `UtilKt`**. Java calls `UtilKt.foo()`. - **Properties** compile to a private backing field plus **`getX()`/`setX()`** accessors. Java uses the getters/setters, not the field. - **`companion object`** members are, by default, instance methods reached via the synthetic `Companion` field: `MyClass.Companion.bar()`. - **`object` (singleton)** members are reached via the `INSTANCE` field. - **Default parameter values** compile to one real method plus a synthetic `name$default(...)` helper that Java cannot call directly. ## Interop annotations These annotations reshape the generated bytecode for Java callers: - **`@JvmStatic`** — on a `companion`/`object` member, emits a genuine `static` method so Java writes `MyClass.bar()` instead of `MyClass.Companion.bar()`. - **`@JvmField`** — exposes a property as a **public field** (no getter/setter); useful for constants and interop with frameworks expecting fields. - **`@JvmOverloads`** — for a function with default parameters, generates the full set of **overloaded** methods so Java can omit arguments. - **`@JvmName("...")`** — renames the generated method or file facade; resolves JVM signature clashes (e.g., two functions that erase to the same signature) and lets you rename `FooKt`. - **`@Throws`** — declares checked exceptions in the method's `throws` clause so Java's checked-exception rules see them. - **`@file:JvmName` / `@file:JvmMultifileClass`** — control the facade class name and merge multiple files into one facade. ```kotlin @file:JvmName("Strings") package util fun isPalindrome(s: String): Boolean = s == s.reversed() // Java: Strings.isPalindrome("abc") class Service { companion object { @JvmStatic fun create(): Service = Service() } } // Java: Service.create() (without @JvmStatic: Service.Companion.create()) fun greet(name: String = "world", loud: Boolean = false) { /* ... */ } // add @JvmOverloads to let Java call greet("a") or greet() ``` ## @Metadata The compiler also writes a **`@Metadata`** annotation onto each class capturing Kotlin-only info (nullability, default values, property/function shapes). Kotlin tooling and reflection read it; Java ignores it. ## Why a senior cares When shipping a JVM-target KMP library, you design the public surface for both Kotlin and Java callers. Forgetting `@JvmStatic`/`@JvmOverloads` produces awkward `Companion.` calls and missing overloads in Java; misusing `@JvmField` breaks encapsulation. These annotations are the contract layer between idiomatic Kotlin and Java consumers.

  • Without @JvmStatic, how does Java call a companion object function?
    Via the synthetic Companion field: MyClass.Companion.fn(). @JvmStatic makes it a plain MyClass.fn() static call.
  • Why might you need @JvmName on two Kotlin functions?
    If they erase to the same JVM signature (e.g., List<Int> and List<String> parameters), @JvmName gives them distinct method names to avoid a clash.

saying these in an interview costs you the question

  • Believing Java cannot call Kotlin at all
  • Not knowing top-level functions become a *Kt facade class
  • Confusing @JvmStatic (statics) with @JvmField (field exposure)
  • Thinking @JvmOverloads is needed for Kotlin callers (it's for Java)
  • Ignoring default-parameter $default bridges when reasoning about Java interop

context