Java Interop

Calling Java from Kotlin and making Kotlin pleasant to call from Java: platform types, the @Jvm* annotations, SAM conversions, and checked exceptions.

Platform types @JvmStatic @JvmOverloads @JvmField @JvmName SAM conversion Checked exceptions
โ˜•

Calling Java from Kotlin

Kotlin compiles to the same JVM bytecode as Java, so calling a Java class, method, or field from Kotlin is direct โ€” no adapter layer, no wrapper generation, no bridging code. Kotlin also smooths over a few Java-specific conventions on the way in: a Java getter/setter pair is exposed as a Kotlin property, and Java's checked exceptions don't require a try/catch the way they would in Java itself (more on that below).

Getter/setter pairs become properties Property syntax
// Java class:
// public class Person {
//     public String getName() { return name; }
//     public void setName(String name) { this.name = name; }
// }

// Kotlin sees getName()/setName() and exposes them as a property
val person = Person()
person.name = "Ada"       // calls setName("Ada") under the hood
println(person.name)   // calls getName()

// A Java isXxx() boolean getter is recognized too
// Java: public boolean isActive()
if (person.isActive) { /* ... */ }  // calls isActive()
๐Ÿน
Coming from Go: there's no equivalent situation in Go โ€” no other language's runtime sits directly underneath it the way the JVM sits underneath Kotlin. Think of Java interop as closer to how Go can link against C via cgo, except vastly more seamless: Kotlin and Java share the exact same bytecode and object model, so there's no FFI boundary or marshaling cost at all.
๐Ÿ‘ป

Platform Types

โ„น๏ธ
Platform types are covered in full in Null Safety. The short version, in interop terms: unannotated Java return types show up in Kotlin as neither String nor String? but something in between โ€” Kotlin defers the null-check to you, and the compiler won't stop you from treating a genuinely-null Java return value as non-null.
Treat unannotated Java returns as nullable Defensive default
// Java: public String getMiddleName() { return middleName; }  (no annotation)

// Risky: compiles, but crashes at runtime if getMiddleName() returns null
val length = person.middleName.length

// Defensive: assume nullable until the Java source proves otherwise
val safeLength = person.middleName?.length ?: 0
๐Ÿ”ฉ

@JvmStatic

A companion object's members are, by default, accessed from Java through an intermediate Companion field โ€” MyClass.Companion.doThing(), not the clean MyClass.doThing() a Java developer would expect from something that looks static. @JvmStatic tells the compiler to additionally generate a real static method on the enclosing class, so Java callers get the ergonomics they expect.

Generating a true static method for Java callers @JvmStatic
class MathUtils {
    companion object {
        @JvmStatic
        fun square(n: Int) = n * n

        fun cube(n: Int) = n * n * n  // no @JvmStatic
    }
}

// From Kotlin, both call the same regardless of the annotation:
MathUtils.square(4)
MathUtils.cube(4)

// From Java, the annotation changes what's callable:
// MathUtils.square(4);              // works: true static method
// MathUtils.cube(4);                // ERROR: not visible this way
// MathUtils.Companion.cube(4);      // works: through the Companion field
๐Ÿ’ก
If a Kotlin library or module is meant to be consumed from Java at all, annotate every companion object member intended as a public factory or utility function with @JvmStatic. It costs nothing for Kotlin callers and removes an awkward .Companion. stepping stone for Java ones.
๐ŸŽ›

@JvmOverloads

Java has no concept of a default parameter value, so a Kotlin function with defaults compiles to a single method requiring every argument โ€” Java callers can't omit any of them. @JvmOverloads tells the compiler to additionally generate one overload per default parameter, dropped from the end inward, restoring the flexibility Kotlin callers already had.

Generating overloads for Java callers @JvmOverloads
class Greeter {
    @JvmOverloads
    fun greet(name: String, greeting: String = "Hello", punctuation: String = "!"): String {
        return "$greeting, $name$punctuation"
    }
}

// From Kotlin, unchanged: named/default args work regardless of the annotation
Greeter().greet("Ada")

// From Java, @JvmOverloads generates three real overloads:
// greeter.greet("Ada");
// greeter.greet("Ada", "Hi");
// greeter.greet("Ada", "Hi", "?!");
// Without the annotation, Java would need to pass all three arguments every time
๐Ÿท

@JvmField & @JvmName

Every Kotlin property compiles to a private field plus getter/setter methods, even a trivial one โ€” which is unnecessary overhead when Java code just wants to touch a plain public field directly. @JvmField exposes the backing field itself, skipping the accessor methods entirely. @JvmName is a different, narrower tool: it renames the generated method or property, most often to work around a Java-side naming collision that Kotlin's own overload resolution otherwise wouldn't see.

@JvmField: skip the accessor methods @JvmField
class Config {
    @JvmField
    val version: String = "1.0"
    // Java: config.version (direct field access, no getVersion() call)
    // Without @JvmField, Java would need config.getVersion()
}
@JvmName: resolve a Java-visible name clash @JvmName
// Two top-level functions overloaded on a generic type parameter alone โ€”
// legal in Kotlin (different erased signatures don't matter to Kotlin's
// own overload resolution), but they'd erase to the same JVM signature
@JvmName("filterInts")
fun List<Int>.filterValid(): List<Int> = filter { it > 0 }

@JvmName("filterStrings")
fun List<String>.filterValid(): List<String> = filter { it.isNotBlank() }

// From Kotlin, both are still called filterValid() โ€” @JvmName only
// changes what the method is called from the Java side
๐ŸŽฏ

SAM Conversions

A SAM (Single Abstract Method) interface โ€” Java's Runnable, Comparator<T>, or any custom functional interface โ€” can be satisfied with a Kotlin lambda directly, without an explicit anonymous class. The compiler generates the wrapper for you. This works automatically for Java interfaces; for a Kotlin-defined interface, you need to mark it fun interface to opt in.

Java functional interfaces, called with a lambda SAM conversion
// java.lang.Runnable is a Java SAM interface: void run()
val task: Runnable = Runnable { println("running") }
Thread(task).start()

// Comparator: another common Java SAM
val byLength = Comparator<String> { a, b -> a.length - b.length }
val sorted = listOf("ccc", "a", "bb").sortedWith(byLength)
println(sorted)  // [a, bb, ccc]
fun interface: opting a Kotlin interface into SAM conversion fun interface
// Without `fun`, this would need an explicit object : Validator { ... }
fun interface Validator {
    fun isValid(input: String): Boolean
}

fun check(input: String, validator: Validator) =
    if (validator.isValid(input)) "valid" else "invalid"

// A plain lambda satisfies the interface directly
println(check("hello") { it.isNotBlank() })  // "valid"
๐Ÿšจ

Checked Exceptions

Kotlin has no checked exceptions at all โ€” every exception, including IOException, is unchecked as far as the Kotlin compiler is concerned. Calling a Java method that declares throws IOException compiles without a try/catch in Kotlin, no different from calling one that throws an unchecked RuntimeException. Going the other direction โ€” a Kotlin function called from Java โ€” needs an explicit annotation if you want Java callers to be forced to handle it.

Calling checked-throwing Java, unchecked No forced catch
// Java: public String readFile(String path) throws IOException { ... }

// Kotlin doesn't require a try/catch โ€” it compiles either way
fun loadConfig(): String {
    return fileReader.readFile("config.json")  // no try/catch needed to compile
}

// You can still choose to handle it, same as any exception
fun loadConfigSafely(): String? = try {
    fileReader.readFile("config.json")
} catch (e: IOException) {
    null
}
@Throws: declaring one for Java callers @Throws
// Without @Throws, the compiled bytecode declares no checked exceptions,
// so Java callers wouldn't even be prompted to handle IOException
@Throws(IOException::class)
fun readConfigFile(path: String): String {
    if (!File(path).exists()) {
        throw IOException("config not found: $path")
    }
    return File(path).readText()
}
// Java callers now see "throws IOException" and must catch or declare it,
// matching what a Java-authored method with the same behavior would require
๐Ÿ“‹

Quick Reference

Concept Syntax Notes
Java getter/setter โ†’ propertyobj.name (calls getName/setName)Automatic; isXxx() โ†’ obj.isXxx too
Platform typeString! (tooling display only)Unannotated Java return; treat as nullable
True static method@JvmStatic on a companion memberAvoids MyClass.Companion.f() from Java
Generated overloads@JvmOverloads on a function with defaultsOne overload per trailing default parameter
Raw field access@JvmField val x = ...Skips getter/setter generation
Rename for Java@JvmName("newName")Resolves JVM-signature clashes; no effect in Kotlin
SAM conversion (Java)val r: Runnable = Runnable { ... }Automatic for any Java functional interface
SAM conversion (Kotlin)fun interface Validator { ... }Opts a Kotlin interface into lambda conversion
Checked exceptionsnone in KotlinCalling throws-declaring Java needs no try/catch
Declare for Java callers@Throws(IOException::class)Makes Java see and enforce the checked exception