Calling Java from Kotlin
seamless ยท same bytecode targetKotlin 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).
// 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()
Platform Types
Type! ยท unchecked nullabilityString 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.// 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
companion object โ true staticA 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.
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
@JvmStatic. It costs nothing for Kotlin callers and removes an awkward .Companion. stepping stone for Java ones.@JvmOverloads
default args โ real overloadsJava 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.
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
expose raw field ยท rename for JavaEvery 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.
class Config { @JvmField val version: String = "1.0" // Java: config.version (direct field access, no getVersion() call) // Without @JvmField, Java would need config.getVersion() }
// 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
Single Abstract Method ยท Runnable ยท lambdaA 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.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]
// 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
no checked exceptions ยท @ThrowsKotlin 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.
// 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 }
// 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
Syntax cheat-sheet| Concept | Syntax | Notes |
|---|---|---|
| Java getter/setter โ property | obj.name (calls getName/setName) | Automatic; isXxx() โ obj.isXxx too |
| Platform type | String! (tooling display only) | Unannotated Java return; treat as nullable |
| True static method | @JvmStatic on a companion member | Avoids MyClass.Companion.f() from Java |
| Generated overloads | @JvmOverloads on a function with defaults | One 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 exceptions | none in Kotlin | Calling throws-declaring Java needs no try/catch |
| Declare for Java callers | @Throws(IOException::class) | Makes Java see and enforce the checked exception |