Start Learning
Javaneer
Back to stage
Stage 0·Why Kotlin, and Interop First

Calling Kotlin from Java

The other direction, which needs a little care: @JvmStatic, @JvmOverloads, and @JvmName control how Kotlin's features (default args, companions, top-level functions) appear to Java.

14 min readAdvanced
On this page

The reverse direction - calling Kotlin from Java - also works, but it's the direction that needs a little care. Kotlin has features Java doesn't (default arguments, top-level functions, companion objects, properties), and when Java calls Kotlin, those features have to be expressed in terms Java understands. A handful of @Jvm... annotations let you control exactly how your Kotlin appears to Java callers - essential when you're writing Kotlin that a Java codebase must consume.

The mismatch: Kotlin has things Java lacks

Java sees Kotlin as ordinary bytecode (last lesson), but some Kotlin conveniences translate awkwardly by default:

  • Default arguments - fun greet(name: String = "world") is one function in Kotlin, but Java has no default parameters, so by default Java can only call it with all arguments supplied.
  • Top-level functions - Java can't call a function outside a class, so Kotlin puts it on a FileNameKt class as a static method (which Java must reference by that generated name).
  • Companion objects - Kotlin's replacement for static members lives in a nested Companion object, so Java sees MyClass.Companion.create() rather than MyClass.create().
  • Properties - a Kotlin val/var is a getter/setter to Java, not a field.

These aren't broken - they just look clunky from Java unless you nudge them. That's what the annotations do.

The @Jvm annotations

Four annotations cover almost every "make this nice for Java" need:

  • @JvmStatic - on a companion-object member, generates a real static method so Java calls MyClass.create() instead of MyClass.Companion.create():
class InvoiceFactory {
    companion object {
        @JvmStatic fun create(id: String) = Invoice(id)   // Java: InvoiceFactory.create("A")
    }
}
  • @JvmOverloads - on a function with default arguments, generates overloaded methods for each combination, so Java can call it with fewer arguments:
@JvmOverloads
fun greet(name: String = "world", loud: Boolean = false) { ... }
// Java gets: greet(name, loud), greet(name), and greet()
  • @JvmName - renames the generated class or method Java sees. Useful to give a top-level-functions file a clean class name (@file:JvmName("Pricing") so Java calls Pricing.applyTax() not PricingKt.applyTax()), or to resolve a signature clash.
  • @JvmField - exposes a Kotlin property as a plain public field (no getter) when Java code expects direct field access.

Reach for these when - and only when - your Kotlin is meant to be consumed by Java.

Design for the consumer

The practical rule: if Kotlin code has Java callers, design its public API with them in mind. A pure-Kotlin module with no Java consumers needs none of these annotations - the interop machinery is invisible within Kotlin. But a Kotlin library or shared module used by Java should:

  • Add @JvmStatic/@JvmOverloads to factory methods and functions with defaults, so Java calls read naturally.
  • Use @JvmName/@file:JvmName to give top-level function files a clean class name.
  • Consider whether Java callers need the null-safety guarantees carried across (Kotlin's non-null types emit runtime null checks that protect Java callers too).

This mirrors the API-design principle from the Architecture path: your public surface is a contract, and here the consumer's language shapes what a good contract looks like.

Only annotate the Java-facing surface

Don't sprinkle @JvmStatic/@JvmOverloads everywhere reflexively - they add generated methods and only matter at the Kotlin→Java boundary. Apply them to the public API that Java actually calls, and leave internal Kotlin code clean. A quick check: if a Kotlin member has no Java callers, it needs no @Jvm annotations. When in doubt, write a small Java call site (or check the decompiled bytecode) to see whether the default interop is already ergonomic.

Writing labels in a second language for specific visitors

Pure Kotlin talking to Kotlin is a household speaking its native language - no translation needed. But when Java guests visit and need to use your kitchen (call your Kotlin API), some of your labels confuse them: your one jar marked 'greet (name optional)' means nothing to a guest who can only read fixed labels (no default arguments), and your communal 'statics shelf' is tucked inside a cupboard called 'Companion' they don't think to open. The @Jvm annotations are you adding clear second-language labels for the guests: @JvmOverloads puts out several pre-filled jars so a guest can grab 'greet()' or 'greet(name)'; @JvmStatic moves the shared items onto an obvious shelf labeled the way guests expect; @JvmName renames a cupboard sensibly. You only relabel the things guests actually touch - the rest of your kitchen stays in your own convenient language.

Make a Kotlin API Java-friendly

You wrote a Kotlin utility that a large Java codebase must use: a top-level function fun formatMoney(amount: Double, currency: String = "USD", symbol: Boolean = true): String in a file moneyUtils.kt, and a factory in a companion object: fun of(cents: Long): Money. Currently Java calls are ugly. Show which annotations make each Java-friendly and what the resulting Java calls look like.

Why do you sometimes need @JvmStatic, @JvmOverloads, or @JvmName when calling Kotlin from Java?

Key takeaways

  • Calling Kotlin from Java works but needs care, because Kotlin has features Java lacks: default arguments, top-level functions, companion objects, and properties.
  • @JvmStatic generates a real static from a companion member (Java: MyClass.create() not MyClass.Companion.create()).
  • @JvmOverloads generates overloaded methods for a function with default arguments, so Java can call it with fewer args.
  • @JvmName renames the generated class/method Java sees (e.g. give a top-level-functions file a clean class name); @JvmField exposes a property as a plain field.
  • Design the Java-facing surface with the consumer in mind - annotate only the API Java actually calls, and leave pure-Kotlin code clean.
Was this lesson helpful?
Edit this page on GitHub