Learn Kotlin - Interoperability & Migration
Series/Learn Kotlin/Episode 17
Episode 17 of 23

Learn Kotlin - Interoperability & Migration

This episode masters Kotlin and Java interoperability: calling Java from Kotlin and vice versa, working with legacy Java codebases, nullability annotations and type safety across boundaries, and strategies for migrating a Java project to Kotlin gradually without stopping development.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Kotlin doesn't force you to leave Java behind. Episode 17 covers interoperability: calling Java code from Kotlin and vice versa, working in long-established Java codebases, keeping type safety across boundaries, and migrating projects gradually.

This is the main reason many companies choose Kotlin: adoption can be incremental. One Java file becomes Kotlin, the application keeps running, and the benefits appear gradually without a big-bang rewrite.

After this episode, you'll migrate a Java project to Kotlin with a strategy that is safe and doesn't halt new features.

Calling Java from Kotlin

Seamless Interop Because the Bytecode Is the Same

Because Kotlin compiles to JVM bytecode, Java classes look natural from Kotlin:

KotlinMemakai class Java dari Kotlin
import java.time.LocalDate
import java.util.UUID
 
val id = UUID.randomUUID()
val hari = LocalDate.now()

UUID and LocalDate are Java classes used directly from Kotlin without adapters. The entire Java standard library is open to Kotlin projects — this is why Kotlin can leverage the enormous JVM ecosystem.

Getters, Setters, and Synthetic Properties

Kotlin properties automatically map to Java getters and setters. A Java private field stays private; access goes through methods. A small consequence: a Java method getNama() looks like a nama property from the Kotlin side, making interop feel seamless.

Calling Kotlin from Java

Platform Types and Nullability

When Java calls Kotlin, Kotlin types become platform types — Kotlin doesn't know whether Java treats a value as nullable or not. The compiler gives a flexible warning, but you must handle nulls wisely on the Java side:

Memanggil Kotlin dari Java
String nama = KtSapa.sapa("Budi");

A Kotlin function fun sapa(nama: String): String looks like a static method on the KtSapa class (with the Kt suffix). Its return value becomes a platform type: Java can call it directly, but null is not guaranteed safe on the Kotlin side when the value comes back.

JVM Annotations for Full Control

The @JvmStatic, @JvmOverloads, and @JvmField annotations give you control over how Kotlin code is exposed to Java:

KotlinAnotasi interop JVM
class Util {
    companion object {
        @JvmStatic
        fun bantu(): String = "helper"
 
        @JvmField
        val versi = "1.0"
    }
}

@JvmStatic makes the function accessible as a static method (not Companion.bantu()), and @JvmField exposes the field directly without a getter. @JvmOverloads generates Java overloads for default parameters. These annotations make Kotlin APIs friendly to Java consumers.

Nullability Annotations and Type Safety Across Boundaries

Bridging Nullability

The main interop problem is nullability. Kotlin enforces null safety; Java doesn't. When Java returns a value that can be null, Kotlin sees it as a platform type — it doesn't know for sure. The solution: nullability annotations on the Java side:

KotlinMenangani platform type
// Java: @Nullable String cari()
val hasil = service.cariNama()
val aman = hasil?.uppercase() ?: "tidak ada"

hasil is a platform type that could be null. You handle it with a safe call and the elvis operator as usual. To reduce ambiguity, libraries use @Nullable and @NotNull annotations (from javax.annotation, JSpecify, or Kotlin) that the Kotlin compiler understands.

Strategy at the Boundary

Set a strategy at the boundary points: every function that crosses the JVM boundary must state its nullability explicitly. In new code, declare non-nullable types firmly; in old code, document null behavior and handle it with ?. and ?:.

Migrating a Java Project to Kotlin

Gradual Migration

The key to safe migration is being incremental: convert files one by one while continuously running tests. Start with the files that benefit the most:

  • Data holders: models and POJOs that immediately become data classes.
  • Utility files: code with repeated null checking that becomes extension functions.
  • Tests: tests that become more concise in the Kotlin style.

Gradle can compile a mix of Java and Kotlin in a single module, so no separate modules are needed. Before migrating, make sure test coverage is complete as a safety net.

Migration Aids

IntelliJ IDEA provides automatic Java to Kotlin conversion: select a file, run Convert Java File to Kotlin File, then review the result. The conversion often needs manual adjustments to use Kotlin idioms, but it speeds up the first steps:

Menjalankan test setelah konversi
./gradlew test

./gradlew test validates that the migration didn't break behavior. After conversion, refactor the result manually: replace !! with the elvis operator, take advantage of data classes and scope functions, and replace loops with collection operations.

Keeping Quality During Migration

Some practices that keep migration healthy:

  • Keep old tests and add new Kotlin tests as you convert.
  • Convert from the leaves toward the root so dependencies are ready first.
  • Avoid changing behavior at the same time as converting the language.
  • Review automatic conversion results for missed Kotlin idioms.
  • Keep the build green at every step.

Closing

Episode 17 mastered interoperability and migration: calling Java from Kotlin and vice versa, platform types at the nullability boundary, @JvmStatic and @JvmField annotations, and incremental migration strategies with tests as a safety net.

The key takeaways:

  • Java classes can be used directly from Kotlin without adapters.
  • Platform types appear at the Java boundary and demand wise null handling.
  • @JvmStatic, @JvmOverloads, and @JvmField control exposure to Java.
  • Nullability annotations bridge the difference between type systems.
  • Migrate incrementally file by file with tests green at all times.
  • IntelliJ's automatic conversion speeds up early steps; manual review is a must.

In episode 18 we'll discuss ecosystem extensions — the kotlinx.coroutines, kotlinx.serialization, and Arrow libraries, using Kotlin with Reactor, Spring, and Android Jetpack, Kotlin scripting and CLI applications, and contributing to the Kotlin open-source ecosystem.

Learn Kotlin - Interoperability & Migration | Learn Kotlin