Skip to main content

Kotlin Java Interoperability

One of Kotlin's greatest strengths is its exceptional interoperability with Java. This feature allows developers to gradually migrate existing Java codebases to Kotlin or use both languages side by side in the same project. In this tutorial, we'll explore how Kotlin and Java interact with each other and how you can leverage the strengths of both languages in your projects.

Introduction to Kotlin-Java Interoperability

Kotlin was designed from the ground up with Java interoperability in mind. Both languages compile to JVM bytecode, allowing them to work together seamlessly. This interoperability means you can:

  • Call Java code from Kotlin
  • Call Kotlin code from Java
  • Have mixed-language projects
  • Use existing Java libraries in Kotlin projects
  • Gradually migrate from Java to Kotlin

Let's dive into the specifics of how this works in practice.

Calling Java Code from Kotlin

Kotlin can directly call Java code without any special configuration or wrapper classes. You can import Java classes and use them naturally in your Kotlin code.

Basic Java Class Usage

Consider this simple Java class:

java
// Person.java
public class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

public void celebrateBirthday() {
age++;
System.out.println(name + " is now " + age + " years old!");
}
}

You can use this Java class in Kotlin code without any conversion:

kotlin
// KotlinFile.kt
fun main() {
val person = Person("Alice", 25)
println("Name: ${person.name}") // Kotlin uses properties instead of getters
println("Age: ${person.age}")

person.celebrateBirthday()
println("New age: ${person.age}")
}

Output:

Name: Alice
Age: 25
Alice is now 26 years old!
New age: 26

Java Collections in Kotlin

Kotlin provides enhanced functionality when working with Java collections. For example, Kotlin adds extension functions to Java collections:

kotlin
fun processJavaList() {
// Create a Java ArrayList
val javaList = java.util.ArrayList<String>()

// Add elements
javaList.add("Java")
javaList.add("Kotlin")
javaList.add("Scala")

// Use Kotlin's extension functions on Java collections
println("First element: ${javaList.first()}")
println("Last element: ${javaList.last()}")
println("Contains 'Kotlin': ${javaList.contains("Kotlin")}")

// Use Kotlin's functional operations
val filtered = javaList.filter { it.length > 4 }
println("Elements with length > 4: $filtered")
}

Output:

First element: Java
Last element: Scala
Contains 'Kotlin': true
Elements with length > 4: [Kotlin, Scala]

Handling Java's Nullability

Java doesn't have built-in null safety, so Kotlin treats all Java types as "platform types" (marked with ! in error messages). Platform types can be treated as either nullable or non-nullable in Kotlin:

kotlin
fun handleJavaNulls() {
val javaClass = JavaClass() // Assume this is a Java class with methods

val possiblyNullString = javaClass.getStringThatMightBeNull()

// Option 1: Treat as non-null (risky if actually null)
val length = possiblyNullString.length // Could throw NullPointerException

// Option 2: Safe handling (recommended)
val safeLength = possiblyNullString?.length ?: 0

println("Safe length: $safeLength")
}

Calling Kotlin Code from Java

Kotlin code can also be called from Java, with some special considerations.

Basic Kotlin Class Usage in Java

Consider this Kotlin class:

kotlin
// Person.kt
class Person(val name: String, var age: Int) {
fun celebrateBirthday() {
age++
println("$name is now $age years old!")
}

companion object {
const val MAX_AGE = 150

fun createAdult(name: String): Person {
return Person(name, 18)
}
}
}

Here's how you can use it from Java:

java
// JavaFile.java
public class JavaFile {
public static void main(String[] args) {
// Create a Person instance
Person person = new Person("Bob", 30);

// Access properties
System.out.println("Name: " + person.getName()); // getName() is generated
System.out.println("Age: " + person.getAge()); // getAge() is generated

// Call methods
person.celebrateBirthday();
System.out.println("New age: " + person.getAge());

// Access companion object constants and methods
System.out.println("Max age: " + Person.MAX_AGE);
Person adult = Person.Companion.createAdult("Charlie");
System.out.println("Adult name: " + adult.getName());
}
}

Output:

Name: Bob
Age: 30
Bob is now 31 years old!
New age: 31
Max age: 150
Adult name: Charlie

Kotlin Properties in Java

Kotlin properties are exposed as getters and setters in Java:

  • For a property val name: String, Java sees a getName() method
  • For a property var age: Int, Java sees both getAge() and setAge(int) methods

Kotlin Extension Functions in Java

Kotlin extension functions are compiled as static methods that take the receiver as the first parameter. To call a Kotlin extension function from Java, you need to call the static method directly:

kotlin
// StringExtensions.kt
package extensions

fun String.addExclamation(): String {
return this + "!"
}

In Java:

java
// JavaExtensionUsage.java
import extensions.StringExtensionsKt;

public class JavaExtensionUsage {
public static void main(String[] args) {
String result = StringExtensionsKt.addExclamation("Hello");
System.out.println(result); // Outputs: Hello!
}
}

Special Interoperability Features

@JvmField Annotation

By default, Kotlin properties are accessed through getters/setters in Java. If you want Java code to access a property directly as a field, use the @JvmField annotation:

kotlin
class KotlinClass {
@JvmField
var publicField = "Directly accessible"
}

In Java:

java
KotlinClass kClass = new KotlinClass();
System.out.println(kClass.publicField); // Direct access, no getter needed
kClass.publicField = "New value"; // Direct access, no setter needed

@JvmStatic Annotation

To make companion object methods appear as true static methods in Java, use @JvmStatic:

kotlin
class Utils {
companion object {
@JvmStatic
fun helper() = "Helper method"

fun nonStaticHelper() = "Non-static helper"
}
}

In Java:

java
// Static access
String result1 = Utils.helper();

// Non-static requires Companion
String result2 = Utils.Companion.nonStaticHelper();

@JvmOverloads Annotation

Kotlin's default parameters aren't visible to Java. Use @JvmOverloads to generate overloaded methods for Java:

kotlin
class Greeter {
@JvmOverloads
fun greet(name: String = "World", greeting: String = "Hello") {
println("$greeting, $name!")
}
}

In Java:

java
Greeter greeter = new Greeter();
greeter.greet(); // Uses both defaults: "Hello, World!"
greeter.greet("Friend"); // Uses second default: "Hello, Friend!"
greeter.greet("Friend", "Hi"); // Uses no defaults: "Hi, Friend!"

Named Arguments

Named arguments in Kotlin aren't preserved when called from Java. Java must call functions with positional arguments:

kotlin
// Kotlin
fun createUser(name: String, age: Int, isAdmin: Boolean) {
// ...
}

// In Kotlin, you can use:
createUser(name = "Alice", isAdmin = true, age = 30)
java
// In Java, you must use:
createUser("Alice", 30, true);

Real-World Example: Building a Task Management API

Let's create a more complex example that demonstrates interoperability in a practical setting. We'll create a task management system with a mix of Kotlin and Java classes.

First, let's create a Java class for our Task entity:

java
// Task.java
public class Task {
private String id;
private String title;
private String description;
private boolean completed;

public Task(String id, String title, String description) {
this.id = id;
this.title = title;
this.description = description;
this.completed = false;
}

// Getters and setters
public String getId() { return id; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public boolean isCompleted() { return completed; }
public void setCompleted(boolean completed) { this.completed = completed; }

@Override
public String toString() {
return "Task{" +
"id='" + id + '\'' +
", title='" + title + '\'' +
", completed=" + completed +
'}';
}
}

Now, let's create a task repository in Kotlin that will use this Java class:

kotlin
// TaskRepository.kt
class TaskRepository {
private val tasks = mutableMapOf<String, Task>()

fun addTask(title: String, description: String): Task {
val id = generateId()
val task = Task(id, title, description)
tasks[id] = task
return task
}

fun getTask(id: String): Task? = tasks[id]

fun getAllTasks(): List<Task> = tasks.values.toList()

fun updateTask(id: String, title: String? = null, description: String? = null, completed: Boolean? = null): Task? {
val task = tasks[id] ?: return null

title?.let { task.title = it }
description?.let { task.description = it }
completed?.let { task.completed = it }

return task
}

fun deleteTask(id: String): Boolean = tasks.remove(id) != null

@JvmOverloads
fun searchTasks(query: String = "", onlyCompleted: Boolean = false): List<Task> {
return tasks.values.filter { task ->
(task.title.contains(query, ignoreCase = true) ||
task.description.contains(query, ignoreCase = true)) &&
(!onlyCompleted || task.isCompleted)
}
}

private fun generateId(): String = java.util.UUID.randomUUID().toString()
}

Finally, let's create a Java class that will use our Kotlin repository:

java
// TaskManager.java
public class TaskManager {
private final TaskRepository repository = new TaskRepository();

public void demo() {
// Add tasks
Task task1 = repository.addTask("Complete Java interop tutorial", "Learn about Kotlin-Java interoperability");
Task task2 = repository.addTask("Buy groceries", "Milk, eggs, bread");
Task task3 = repository.addTask("Exercise", "Go for a 30-minute run");

// Update a task
repository.updateTask(task1.getId(), null, null, true);

// Search tasks
System.out.println("All tasks:");
for (Task task : repository.getAllTasks()) {
System.out.println(task);
}

System.out.println("\nCompleted tasks:");
for (Task task : repository.searchTasks("", true)) {
System.out.println(task);
}

System.out.println("\nTasks containing 'e':");
for (Task task : repository.searchTasks("e", false)) {
System.out.println(task);
}
}

public static void main(String[] args) {
new TaskManager().demo();
}
}

Output:

All tasks:
Task{id='d3f5a8e1-b8c9-4d6e-9f7a-2c3b4d5e6f7a', title='Complete Java interop tutorial', completed=true}
Task{id='a1b2c3d4-e5f6-4a5b-9c8d-7e6f5d4c3b2a', title='Buy groceries', completed=false}
Task{id='1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d', title='Exercise', completed=false}

Completed tasks:
Task{id='d3f5a8e1-b8c9-4d6e-9f7a-2c3b4d5e6f7a', title='Complete Java interop tutorial', completed=true}

Tasks containing 'e':
Task{id='d3f5a8e1-b8c9-4d6e-9f7a-2c3b4d5e6f7a', title='Complete Java interop tutorial', completed=true}
Task{id='a1b2c3d4-e5f6-4a5b-9c8d-7e6f5d4c3b2a', title='Buy groceries', completed=false}
Task{id='1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d', title='Exercise', completed=false}

Common Pitfalls and Solutions

1. Java's checked exceptions

Kotlin doesn't have checked exceptions, but when calling Java methods that throw checked exceptions, you still need to handle them:

kotlin
fun readFile(path: String) {
try {
val file = java.io.File(path)
val lines = file.readLines() // This Java method throws IOException
println(lines)
} catch (e: java.io.IOException) {
println("Error reading file: ${e.message}")
}
}

2. Static members

Access Java static fields and methods directly through the class name:

kotlin
val random = java.lang.Math.random()
val maxInt = java.lang.Integer.MAX_VALUE

3. SAM Conversions

Kotlin supports Single Abstract Method (SAM) conversions for Java interfaces:

kotlin
// Java interface with a single method
// public interface Runnable { void run(); }

// In Kotlin, you can use lambda expressions
val runnable = Runnable { println("Running in a thread") }

// Use with Java APIs
val thread = Thread(runnable)
thread.start()

Summary

Kotlin-Java interoperability is one of Kotlin's strongest features, allowing you to:

  • Seamlessly call Java code from Kotlin and Kotlin code from Java
  • Leverage the extensive Java ecosystem while using Kotlin's modern features
  • Gradually migrate Java projects to Kotlin
  • Mix both languages in the same project

Key points to remember:

  1. Java classes and methods can be used directly in Kotlin
  2. Kotlin properties appear as getters/setters in Java
  3. Kotlin's null safety translates into runtime checks in Java
  4. Special annotations like @JvmStatic, @JvmField, and @JvmOverloads improve Java interoperability
  5. Kotlin extension functions appear as static methods in Java

Additional Resources

Exercises

  1. Create a simple Java class with multiple methods and access it from Kotlin code.
  2. Write a Kotlin class with a companion object and use it from Java.
  3. Create a Kotlin extension function for a Java standard library class and call it from both Kotlin and Java.
  4. Build a small application with a mix of Java and Kotlin classes that work together.
  5. Take an existing Java method with multiple parameters and refactor it in Kotlin using default parameters. Make it Java-friendly with @JvmOverloads.

By mastering Kotlin-Java interoperability, you'll be able to leverage the best of both languages in your projects while ensuring a smooth transition path for existing Java codebases.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)