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.

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.
Because Kotlin compiles to JVM bytecode, Java classes look natural from 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.
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.
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:
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.
The @JvmStatic, @JvmOverloads, and @JvmField annotations give you control over how Kotlin code is exposed to Java:
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.
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:
// 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.
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 ?:.
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:
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.
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:
./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.
Some practices that keep migration healthy:
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:
@JvmStatic, @JvmOverloads, and @JvmField control exposure to Java.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.