Java Interoperability with Kotlin for Modern Applications
Seamlessly integrate Kotlin into existing Java codebases to modernize applications without rewriting
Kotlin has rapidly gained popularity as a modern, concise, and expressive alternative to Java — especially in Android development and modern JVM backends. However, most enterprise and legacy codebases are still built in Java. Rewriting them from scratch isn’t always practical.
The good news? Kotlin is fully interoperable with Java. This means you can gradually adopt Kotlin, mix it with existing Java code, and enjoy modern language features without disrupting your architecture.
In this guide, we’ll explore real-world interoperability between Java and Kotlin, covering method calls, nullability, collections, lambdas, and best practices for maintaining hybrid codebases.
Why Interoperate Java and Kotlin?
Kotlin runs on the JVM and compiles to bytecode, just like Java. That means:
- You can call Java code from Kotlin and vice versa
- Use existing Java libraries in Kotlin projects
- Share business logic between Kotlin and Java modules
- Gradually migrate legacy systems without a full rewrite
Calling Java from Kotlin
Java classes are accessible from Kotlin with no special syntax:
// Java
public class LegacyService {
public String greet(String name) {
return "Hello, " + name;
}
}
// Kotlin
val service = LegacyService()
println(service.greet("Alice"))
Static methods are accessed like companion objects:
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
val sum = MathUtils.add(5, 10)
Calling Kotlin from Java
Kotlin code can be accessed from Java, but with a few things to watch for.
Top-level functions are compiled into a class named after the filename:
// File: Utils.kt
fun multiply(a: Int, b: Int) = a * b
In Java:
int result = UtilsKt.multiply(3, 4);
To rename the wrapper class, use @file:JvmName("Utils")
.
Null Safety and Type Annotations
Java doesn’t enforce nullability at compile time. Kotlin does.
Kotlin assumes Java types are platform types (String!
) which could be null or not. Use annotations in Java to improve Kotlin’s null safety inference:
public class Api {
@Nullable
public String getNullableValue() { return null; }
@NotNull
public String getNonNullValue() { return "safe"; }
}
In Kotlin:
val a: String? = api.getNullableValue() // safe
val b: String = api.getNonNullValue() // smart cast
Working with Collections
Java and Kotlin collections are interoperable, but Kotlin differentiates between mutable and immutable collections.
From Java:
List<String> names = new ArrayList<>();
names.add("John");
names.add("Jane");
In Kotlin:
val kotlinList: MutableList<String> = names.toMutableList()
kotlinList.add("Jake")
Kotlin’s List<String>
is read-only. Use MutableList
to make changes.
Interoperability with Lambdas and SAM Conversions
Kotlin can use Java interfaces with single abstract methods (SAM interfaces) as lambdas:
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// handle click
}
});
In Kotlin:
button.setOnClickListener {
// handle click
}
However, calling Kotlin lambdas from Java requires care. Kotlin uses FunctionX
interfaces under the hood. If needed in Java, expose them as regular interfaces using @FunctionalInterface
.
Kotlin Annotations to Aid Interoperability
@JvmStatic
: Exposes a method as static in the companion object@JvmOverloads
: Generates overloads for default parameters@JvmField
: Exposes a Kotlin property as a public Java field@Throws
: Declares checked exceptions for Java consumers
Example:
class Utils {
companion object {
@JvmStatic
fun greet(name: String) = "Hi $name"
}
}
Usage in Java:
String message = Utils.greet("Sam");
Best Practices for Java–Kotlin Interop
- Use Kotlin annotations (
@Jvm...
) to improve Java accessibility - Annotate Java APIs with nullability annotations for Kotlin consumers
- Refactor Java classes before migrating to Kotlin
- Avoid exposing Kotlin-specific features (e.g., inline classes, coroutines) directly to Java
- Keep interfaces in Java if your codebase has mixed language usage
Conclusion
Kotlin–Java interoperability makes Kotlin adoption seamless in existing JVM applications. Whether you’re modernizing an old codebase or incrementally adding Kotlin to new services, this bi-directional compatibility empowers teams to move fast without starting from scratch.
By understanding the interoperability mechanics and applying best practices, you can create maintainable, modern applications that leverage the best of both languages.