Phase 7 · Professional & AppliedModule 35~34 min read

Java Interoperability

Mix Kotlin and Java seamlessly in the same project.

What you'll learn

Kotlin was built for 100% Java interoperability. It compiles to the same bytecode, runs on the same JVM, and can freely use the enormous Java ecosystem — while Java code can call back into your Kotlin. This is exactly why teams can adopt Kotlin gradually, one file at a time.

By the end you'll be able to:

  • Call Java classes and libraries from Kotlin
  • Handle platform types and their nullability risk
  • Make Kotlin pleasant to call from Java with JVM annotations
  • Use SAM conversions to pass lambdas to Java interfaces

Calling Java from Kotlin

Java classes are used just like Kotlin ones — no wrappers, no glue. Kotlin even improves the ergonomics: Java getters and setters show up as Kotlin properties, and Java collections gain Kotlin's extension functions:

JavaFromKotlin.kt
import java.util.ArrayList

fun main() {
    val list = ArrayList<String>()      // a Java class, used like any other
    list.add("Kotlin")
    list.add("Java")

    // Java getFoo()/setFoo() appear as Kotlin properties:
    println("size = ${list.size}")     // list.size() called as a property
    for (s in list) println(s)
}

Platform types & nullability

Java has no built-in null safety, so when Kotlin calls Java it can't always know whether a value may be null. Such a value gets a platform type (written String! in errors): Kotlin relaxes its checks and trusts you. That's convenient but dangerous — a null can slip through and NPE at runtime, the very thing Kotlin usually prevents:

Platform.kt
// A Java method with no @Nullable/@NotNull returns a "platform type",
// shown in errors as String! — Kotlin trusts you not to break it.

val name: String = user.getName()   // compiles, but NPEs if Java returns null!

// Safer: treat the result as nullable and handle it explicitly:
val safe: String? = user.getName()
println(safe?.uppercase() ?: "unknown")

Watch out

At the Java boundary, be pessimistic: assume return values can be null and type them as String?. Well-annotated Java libraries (using @Nullable/@NotNull) let Kotlin infer nullability correctly, so prefer those.

Calling Kotlin from Java

The other direction usually just works, but Kotlin features that don't exist in Java (top-level functions, default arguments, companion objects) need a nudge. A few annotations make Kotlin APIs feel natural from Java:

MathUtils.kt
// MathUtils.kt
object MathUtils {
    @JvmStatic                        // expose as a real static method
    fun square(x: Int) = x * x
}

class Greeter @JvmOverloads constructor(  // generate overloads for defaults
    val greeting: String = "Hello",
)
Caller.java
// From Java code:
int r = MathUtils.square(5);          // @JvmStatic: no INSTANCE needed
Greeter g = new Greeter();            // @JvmOverloads: no-arg ctor exists
System.out.println(g.getGreeting());  // Kotlin 'val' -> Java getter
AnnotationWhat it does
@JvmStaticexposes a companion/object member as a real Java static method
@JvmOverloadsgenerates Java overloads for Kotlin default arguments
@JvmFieldexposes a property as a plain public field (no getter/setter)
@JvmName("...")renames the generated Java method/class (e.g. to avoid clashes)

SAM conversions

A Java SAM (Single Abstract Method) interface — like Runnable, Comparator, or a listener — can be created straight from a Kotlin lambda. This makes Java callback APIs feel modern:

Sam.kt
// A Java interface with a Single Abstract Method (SAM) accepts a lambda:
val r = Runnable { println("running on a thread") }
Thread(r).start()

// You can pass the lambda directly too:
Thread { println("also works") }.start()

Note

SAM conversion applies to Java interfaces automatically. For Kotlin, an ordinary interface needs the fun interface keyword to opt into lambda conversion — Kotlin prefers explicit function types otherwise.

Recap & quick check

Key takeaways

  • Kotlin has full two-way interop with Java: same bytecode, same JVM, shared ecosystem.
  • Java getters/setters appear as Kotlin properties; Java classes are used with no wrappers.
  • Values from unannotated Java are platform types (String!) — treat them as nullable to stay safe.
  • @JvmStatic, @JvmOverloads, and @JvmField make Kotlin APIs comfortable to call from Java.
  • Java SAM interfaces accept Kotlin lambdas directly; Kotlin interfaces need 'fun interface' to do the same.

Quick check

1. What is a 'platform type' like String!?

2. How does a Java getName()/setName() pair appear in Kotlin?

3. Which annotation generates Java overloads for Kotlin default arguments?

4. What is a SAM conversion?

5. How should you treat return values from unannotated Java methods?

With interop mastered you can use any JVM library. Next we'll prove our code actually works with professional testing — JUnit 5, Kotest, MockK, and testing coroutines.