Java Interop
Reviewed & published by Brayan K
Java interop is Kotlin's ability to call Java code and be called by Java with no glue layer, sharing the same JVM, classes, and libraries seamlessly in both directions.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
You'll handle Java's platform types and null, map getters to properties, and use @JvmStatic, @JvmOverloads, and @JvmField to make Kotlin friendly to Java callers.
What You'll Learn in This Lesson
- Calling Java classes and statics from Kotlin
- Platform types and handling Java null safely
- Java getters/setters mapping to Kotlin properties
- @JvmStatic, @JvmOverloads, @JvmField, @JvmName
- SAM conversions: lambdas for Java interfaces
- How Unit and void map across the boundary
1️⃣ Calling Java From Kotlin
Because both compile to JVM bytecode, Kotlin uses Java classes directly — every example here calls the JDK, which is Java. Java's getter/setter pairs appear as Kotlin properties (so sb.length, not sb.getLength()), and Java statics are called as usual.
// Calling Java (here, the JDK) from Kotlin is seamless.
// Java getters/setters map to Kotlin PROPERTY syntax.
fun main() {
val sb = StringBuilder() // a Java class
sb.append("Hi ")
sb.append("there")
// Java's getLength() / length() reads like a property in Kotlin:
println(sb.length) // -> 8
// Java's static methods are called normally:
println(Math.max(3, 9)) // -> 9
println(Integer.parseInt("42")) // -> 42
}2️⃣ Platform Types and Null
Java carries no nullability information, so a Java return value arrives as a platform type (String!). Kotlin lets you treat it as nullable or non-null — but if you assume non-null and it's actually null, you get a crash. The safe habit: treat it as nullable and guard with ?:.
// Java has no nullability info, so its return types become
// "platform types" in Kotlin (shown as String!). YOU decide null.
fun main() {
// System.getProperty returns a Java String (could be null).
// We treat it as nullable and guard with the Elvis operator.
val sep: String? = System.getProperty("nonexistent.key")
val safe = sep ?: "(missing)"
println(safe) // -> (missing)
// A property that always exists:
val version = System.getProperty("java.specification.version")
println(version != null) // -> true
}3️⃣ Making Kotlin Friendly to Java
When Java code will call your Kotlin, a few annotations smooth the path. @JvmStatic generates real statics from companion members; @JvmOverloads generates overloads for default arguments; @JvmField exposes a plain field; @JvmName renames a method as Java sees it.
// These @Jvm* annotations control how Kotlin LOOKS to Java callers.
// They change generated bytecode, not Kotlin behaviour.
class Pricing {
companion object {
// @JvmStatic -> Java can call Pricing.taxRate() (real static)
@JvmStatic
fun taxRate(): Double = 0.2
}
}
// @JvmOverloads -> generates overloads for default args,
// so Java can call greet("Ada") OR greet("Ada", "Hello").
@JvmOverloads
fun greet(name: String, prefix: String = "Hi"): String = "$prefix, $name"
fun main() {
// From Kotlin we just call them normally:
println(Pricing.taxRate()) // -> 0.2
println(greet("Ada")) // -> Hi, Ada
println(greet("Ada", "Hello"))// -> Hello, Ada
}From Kotlin these calls look ordinary; the annotations only change the bytecode Java sees. Lambdas also auto-convert to single-method Java interfaces — a SAM conversion — so executor.execute { ... } just works.
Your turn. Fill in the ___ blanks, then run and compare.
// 🎯 YOUR TURN — replace each ___ then run main()
fun main() {
// 1) Java's StringBuilder length reads as a PROPERTY in Kotlin
val sb = StringBuilder("Kotlin")
println(sb.___) // 👉 put: length (prints 6)
// 2) System.getProperty may be null — default with Elvis
val v = System.getProperty("missing") ___ "none" // 👉 put: ?:
println(v) // prints none
// ✅ Expected output:
// 6
// none
}Mini-Challenge: Interop-Friendly Kotlin
Expose a @JvmStatic function and a @JvmOverloads default-argument function.
// 🎯 MINI-CHALLENGE: interop-friendly Kotlin.
// Save as Interop.kt and run main().
//
// 1. A companion object function area() returning 50, marked @JvmStatic.
// 2. A top-level fun label(name: String, suffix: String = "!") with
// @JvmOverloads so Java can omit the second arg.
// 3. In main(), print area(), label("Box"), label("Box", "?").
//
// ✅ Expected output:
// 50
// Box!
// Box?
class Shape {
companion object {
@JvmStatic
fun area(): Int = 50
}
}
@JvmOverloads
fun label(name: String, suffix: String = "!"): String = name + suffix
fun main() {
// print the three lines
}Common Errors (and the fix)
- ❌ NPE from a Java return value — you assumed a platform type was non-null. ✅ Treat it as nullable and guard with ?: or ?..
- ❌ Java can't see your companion function — it had to write .Companion.. ✅ Add @JvmStatic.
- ❌ Java must pass every argument — defaults aren't visible. ✅ Add @JvmOverloads.
- ❌ "Accidental override" / name clash from Java — two signatures collide. ✅ Use @JvmName to rename one.
Pro Tips
- 💡 Honour @Nullable/@NotNull: if the Java library is annotated, Kotlin enforces it — no platform-type guesswork.
- 💡 fun interface for SAM: declare your own single-method interface with fun interface so Kotlin callers can pass a lambda.
- 💡 Unit maps to void: a Kotlin function returning Unit looks like a normal void method to Java.
📋 Quick Reference — Interop Annotations
| Annotation | Makes Java see |
|---|---|
| @JvmStatic | a real static member |
| @JvmOverloads | overloads for default args |
| @JvmField | a plain public field |
| @JvmName("x") | a renamed method/file |
| String! | a platform type (null unknown) |
| Unit | void |
🎉 Lesson Complete!
- ✅ Kotlin calls Java directly; getters/setters become properties
- ✅ Java values arrive as platform types — guard them as nullable
- ✅ @JvmStatic and @JvmOverloads make Kotlin friendly to Java
- ✅ @JvmField and @JvmName control fields and names
- ✅ Lambdas SAM-convert to Java interfaces; Unit maps to void
Practice quiz
How does Kotlin see a Java getLength()/setLength() pair?
- As a property named length
- As two unrelated methods
- As a constructor
- As an interface
Answer: As a property named length. Kotlin maps JavaBeans getX/setX to property syntax, so it reads as length.
What is a 'platform type' in Kotlin?
- A type that only runs on Android
- A Java type whose nullability is unknown to Kotlin
- A reified generic type
- A sealed class
Answer: A Java type whose nullability is unknown to Kotlin. Java carries no nullability info, so its types arrive as platform types (String!).
What is the safe habit when using a Java return value?
- Always assume it is non-null
- Cast it to Any
- Treat it as nullable and guard it
- Wrap it in lazy
Answer: Treat it as nullable and guard it. Treat platform types as nullable and guard with ?: or ?. to avoid an NPE.
What does @JvmStatic on a companion-object function do?
- Makes it private
- Removes it from bytecode
- Makes it nullable
- Generates a real static method Java can call directly
Answer: Generates a real static method Java can call directly. @JvmStatic generates a real static so Java calls MyClass.member() directly.
What does @JvmOverloads generate for a function with default arguments?
- Overloads so Java can omit the defaulted parameters
- A nullable return type
- A static field
- A coroutine
Answer: Overloads so Java can omit the defaulted parameters. @JvmOverloads generates overloads omitting defaults, so Java can use shorter calls.
What does @JvmField expose a property as?
- A private method
- A plain public field with no getter/setter
- A computed property
- A companion object
Answer: A plain public field with no getter/setter. @JvmField exposes the property as a plain public field, bypassing get/set.
What is a SAM conversion?
- Casting a String to Any
- Converting Unit to void
- Passing a lambda where a single-method interface is expected
- Renaming a method for Java
Answer: Passing a lambda where a single-method interface is expected. SAM conversion lets a lambda stand in for a single-abstract-method interface.
How does a Kotlin function returning Unit appear to Java?
- As returning Object
- As returning Unit
- As a void method
- As returning null
Answer: As a void method. Kotlin maps Unit to Java's void for methods exposed to Java callers.
What does @JvmName let you control?
- The name a method or file has as seen from Java
- The nullability of a type
- The thread a function runs on
- The default argument values
Answer: The name a method or file has as seen from Java. @JvmName renames the generated method or file as Java sees it, resolving clashes.
How do you let your own Kotlin interface accept a lambda like a Java SAM?
- Mark it @JvmStatic
- Declare it with 'fun interface'
- Make it abstract
- Add @JvmField
Answer: Declare it with 'fun interface'. Declaring a single-method interface as a 'fun interface' enables lambda use in Kotlin.
Continue this course
- Previous: Testing in Kotlin (JUnit & kotlin.test)
- Next: Checkpoint: Idiomatic Kotlin — Combine a value class, operator overload and a DSL in a build — then a quiz
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What are platform types and why do they matter?
When Kotlin calls Java, it usually has no information about whether a Java value can be null, because plain Java types carry no nullability. Kotlin represents these as 'platform types', written internally as String! (with an exclamation mark you cannot type yourself). A platform type relaxes Kotlin's null checks: you may treat it as either nullable or non-null, and the compiler trusts you. The catch is that if you assume non-null and the Java value is actually null, you get a NullPointerException at runtime. The safe habit is to treat Java return values as nullable and guard them, or check the Java method's contract and use @Nullable/@NotNull annotations when available.
How do Java getters and setters appear in Kotlin?
Kotlin maps the JavaBeans convention to property syntax. A Java getX()/setX(value) pair is seen by Kotlin as a property x, so you write obj.x and obj.x = value instead of obj.getX() and obj.setX(value). A boolean isX() getter maps to a property named x as well. Methods that don't follow the get/set naming stay as ordinary method calls. This is why calling the JDK from Kotlin often reads more cleanly than the original Java — sb.length looks like a property even though it is backed by a method.
What do @JvmStatic, @JvmField, and @JvmName do?
These annotations shape how Kotlin appears to Java callers. @JvmStatic on a companion-object member generates a real static method/field so Java writes MyClass.member() instead of MyClass.Companion.member(). @JvmField exposes a property as a plain public field with no getter/setter, useful for constants and interop with frameworks that read fields directly. @JvmName lets you rename the generated method or file as seen from Java, which resolves clashes (for example two functions whose signatures erase to the same JVM signature). None of them change how the code behaves when called from Kotlin.
What is @JvmOverloads for?
Kotlin supports default parameter values, but Java does not, so a Kotlin function fun greet(name: String, prefix: String = "Hi") compiles to a single two-argument method that Java callers must always pass both arguments to. Adding @JvmOverloads tells the compiler to also generate the overloads that omit the defaulted parameters, so Java can call greet("Ada") as well as greet("Ada", "Hello"). It is essential whenever you expose a function with default arguments to Java code and want the convenient shorter calls to be available there too.
What is a SAM conversion?
SAM stands for Single Abstract Method. Many Java interfaces (like Runnable or a listener with one method) have exactly one abstract method, and Kotlin lets you pass a lambda where such a Java interface is expected — the compiler automatically converts the lambda into an instance of the interface. So you can write executor.execute { doWork() } instead of constructing an anonymous Runnable. This works automatically for Java SAM interfaces; for Kotlin's own interfaces you opt in by declaring them with the 'fun interface' keyword, after which lambdas can be used the same way.
How do Unit and void relate across the boundary?
Kotlin functions that return nothing have the return type Unit, which is a real type with a single value. When such a function is exposed to Java, Kotlin maps Unit to Java's void for methods, so Java callers see an ordinary void method and need not deal with a Unit value. In the other direction, a Java method declared void is seen in Kotlin as returning Unit. The practical upshot is that 'returns nothing' works naturally in both languages; you only notice Unit explicitly in Kotlin when you use it as a generic type argument or a function type's result.