Java interoperability

Kotlin · Advanced · 12 min

Most Kotlin code runs alongside Java libraries and frameworks; smooth interop is a key Kotlin strength.

Calling Java from Kotlin mostly just works:

  • Java getters and setters appear as properties: user.getName() / setName() become user.name.
  • Java types have platform types (String!): Kotlin doesn't know their nullability, so it lets you treat them as nullable or not, and a wrong guess fails at runtime. Declare the Kotlin type explicitly at the boundary (val name: String? = javaObj.name). Nullability annotations (@Nullable, JSpecify @NullMarked) on the Java side give Kotlin real types.
  • SAM conversion: a lambda can be passed where a Java single-abstract-method interface (Runnable, Comparator) is expected. For Kotlin interfaces, declare them fun interface to get the same.
  • Java's checked exceptions don't need to be caught.
  • Escape Kotlin keywords used as Java names with backticks: `Mockito.when(...)`.

Calling Kotlin from Java needs a few annotations:

  • Top-level functions in Utils.kt are static methods of UtilsKt; rename with @file:JvmName("Utils").
  • @JvmStatic on companion or object members creates real static methods; @JvmField exposes a property as a public field.
  • @JvmOverloads generates overloads for default arguments, which Java can't use otherwise.
  • @Throws(IOException::class) declares checked exceptions for Java callers.

Kotlin collections map to Java's (List is java.util.List); read-only is a compile-time view only, so Java code can still mutate them.

Example

import java.util.Optional
import java.util.concurrent.Executors

fun interface Validator { fun validate(s: String): Boolean }   // SAM for Kotlin
object MathUtils {
    @JvmStatic fun twice(x: Int) = x * 2                        // static for Java
    @JvmOverloads fun greet(name: String, greeting: String = "Hi") = "$greeting $name"
}
fun main() {
    val sb = java.lang.StringBuilder()
    sb.append("abc")
    println(sb.length)                       // 3 (Java length() as a property)
    val names = java.util.ArrayList<String>()
    names.add("b"); names.add("a")
    names.sortWith { x, y -> x.compareTo(y) } // lambda -> java.util.Comparator
    println(names)                           // [a, b]
    val env: String? = System.getenv("NOT_SET")  // platform type made explicit
    println(env ?: "unset")                  // unset
    val notBlank = Validator { it.isNotBlank() }
    println(notBlank.validate(" "))          // false
    println(Optional.ofNullable<String>(null).orElse("none")) // none
    val pool = Executors.newSingleThreadExecutor()
    println(pool.submit<Int> { MathUtils.twice(21) }.get())  // 42
    pool.shutdown()
}

Remember

  • Java getters/setters become Kotlin properties
  • Platform types (T!) have unknown nullability: declare the type at the boundary
  • Lambdas convert to Java SAM interfaces; use fun interface in Kotlin
  • @JvmStatic, @JvmOverloads, @JvmName shape the Java-facing API

Watch out for

  • Letting a Java method's platform-typed null flow into a non-null Kotlin variable, causing an NPE far from the source.

Check yourself

  1. Java code calls a Kotlin function fun f(a: Int, b: Int = 0). Without annotations, what can Java do?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: kotlinlang.org/docs/java-interop.html

Builds on

Unlocks

Nothing builds on this yet.