article banner (priority)

Kotlin and Java interoperability: Traps and gotchas

In Kotlin, every function declares a return type, so we return Unit in Kotlin instead of the Java void keyword.

fun a() {} fun main() { println(a()) // kotlin.Unit }

Of course, practically speaking it would be inefficient to always return Unit even when it’s not needed; so, functions with the Unit result type are compiled to functions with no result. When their result type is needed, it is injected on the use side.

// Kotlin code fun a(): Unit { return Unit } fun main() { println(a()) // kotlin.Unit }
// Compiled to the equivalent of the following Java code
public static final void a() {
}

public static final void main() {
  a();
  Unit var0 = Unit.INSTANCE;
  System.out.println(var0);
}

This is a performance optimization. The same process is used for Java functions without a result type, therefore they can be treated like they return Unit in Kotlin.

Function types and function interfaces

Using function types from Java can be problematic. Consider the following setListItemListener function, which expects a function type as an argument. Thanks to named arguments, Kotlin will provide proper suggestions and the usage will be convenient.

class ListAdapter { fun setListItemListener( listener: ( position: Int, id: Int, child: View, parent: View ) -> Unit ) { // ... } // ... } // Usage fun usage() { val a = ListAdapter() a.setListItemListener { position, id, child, parent -> // ... } }

Using Kotlin function types from Java is more problematic. Not only are named parameters lost, but also it is expected that the Unit instance is returned.

The solution to this problem is functional interfaces, i.e., interfaces with a single abstract method and the fun modifier. After using functional interfaces instead of function types, the usage of setListItemListener in Kotlin remains the same, but in Java it is more convenient as named parameters are understood and there is no need to return Unit.

fun interface ListItemListener { fun handle( position: Int, id: Int, child: View, parent: View ) } class ListAdapter { fun setListItemListener(listener: ListItemListener) { // ... } // ... } fun usage() { val a = ListAdapter() a.setListItemListener { position, id, child, parent -> // ... } }

Tricky names

Keywords like when or object are reserved in Kotlin, so they cannot be used as names for functions or variables. The problem is that some of these keywords are not reserved in Java, so they might be used by Java libraries. A good example is Mockito, which is a popular mocking library, but one of its most important functions is named "when". To use this function in Kotlin, we need to surround its name with backticks (`).

// Example Mockito usage val mock = mock(UserService::class.java) `when`(mock.getUser("1")).thenAnswer { aUser }

Backticks can also be used to define functions or variable names that would otherwise be illegal in Kotlin. They are most often used to define unit test names with spaces in order to improve their readability in execution reports. Such function names are legal only in Kotlin/JVM, and only if they are not going to be used for code that runs on Android (unit tests are executed locally, so they can be named this way).

class MarkdownToHtmlTest { @Test fun `Simple text should remain unchanged`() { val text = "Lorem ipsum" val result = markdownToHtml(text) assertEquals(text, result) } }

Throws

In Java, there are two types of exceptions:

  • Checked exceptions, which need to be explicitly stated and handled in code. Checked exceptions in Java must be specified after the throws keyword in the declarations of functions that can throw them. When we call such functions from Java, we either need the current function to state that it might throw a specific exception type as well, or this function might catch expected exceptions. In Java, except for RuntimeException and Error, classes that directly inherit Throwable are checked exceptions.
  • Unchecked exceptions, which can be thrown "at any time" and don’t need to be stated in any way, therefore methods don't have to catch or throw unchecked exceptions explicitly. Classes that inherit Error or RuntimeException are unchecked exceptions.
public class JavaClass {
  // IOException are checked exceptions,
  // and they must be declared with throws
  String readFirstLine(String fileName) throws IOException {
    FileInputStream fis = new FileInputStream(fileName);
    InputStreamReader reader = new InputStreamReader(fis);
    BufferedReader bufferedReader = new BufferedReader(reader);
    return bufferedReader.readLine();
  }
  
  void checkFirstLine() {
    String line;
    try {
      line = readFirstLine("number.txt");
      // We must catch checked exceptions, 
      // or declare them with throws
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
    // parseInt throws NumberFormatException,
    // which is an unchecked exception
    int number = Integer.parseInt(line);
    // Dividing two numbers might throw
    // ArithmeticException of number is 0,
    // which is an unchecked exception
    System.out.println(10 / number);
  }
}

In Kotlin, all exceptions are considered unchecked. This leads to a problem when we use Java to call Kotlin methods that throw exceptions that are considered checked exceptions in Java. In Java, such methods must have exceptions specified after the throws keyword. Kotlin does not generate these, therefore Java is confused. If you try to catch such an exception, Java will prohibit it, explaining that such an exception is not expected.

// Kotlin @file:JvmName("FileUtils") package test import java.io.* fun readFirstLine(fileName: String): String = File(fileName).useLines { it.first() }

To solve this issue, in all Kotlin functions that are intended to be used from Java, we should use the Throws annotation to specify all exceptions that are considered checked in Java.

// Kotlin @file:JvmName("FileUtils") package test import java.io.* @Throws(IOException::class) fun readFirstLine(fileName: String): String = File(fileName).useLines { it.first() }

When this annotation is used, the Kotlin Compiler will specify these exceptions in the throws block in the generated JVM functions, therefore they are expected in Java.

Using the Throws annotation is not only useful for Kotlin and Java interoperability; it is also often used as a form of documentation that specifies which exceptions should be expected.

JvmRecord

Java 16 introduced records as immutable data carriers. In simple words, these are alternatives to Kotlin data classes. Java records can be used in Kotlin just like any other kind of class. To declare a record in Kotlin, we define a data class and use the JvmRecord annotation.

@JvmRecord data class Person(val name: String, val age: Int)

Records have more restrictive requirements than data classes. Here are the requirements for the JvmRecord annotation to be used for a class:

  • The class must be in a module that targets JVM 16 bytecode (or 15 if the -Xjvm-enable-preview compiler option is enabled).
  • The class cannot explicitly inherit any other class (including Any) because all JVM records implicitly inherit java.lang.Record. However, the class can implement interfaces.
  • The class cannot declare any properties that have backing fields, except these initialized from the corresponding primary constructor parameters.
  • The class cannot declare any mutable properties that have backing fields.
  • The class cannot be local.
  • The class's primary constructor must be as visible as the class itself.

Summary

Kotlin and Java are two different languages, designed in two different centuries1, which sometimes makes it challenging when we interoperate between them. Most problems relate to important Kotlin features, like eliminating the concept of checked exceptions, or distinguishing between nullable and non-nullable types, between interfaces for read-only and mutable collections, or between shared types for primitives and wrapped primitives. Kotlin does all it can to make interoperability with Java as convenient as possible, but there are some inevitable trade-offs. For example, both the List and MutableList Kotlin types relate to the Java List interface. Also, Kotlin relies on Java nullability annotations and uses platform types when they are missing. We also need to know and use some annotations that determine how our code will behave when used from Java or another JVM language. These are challenges for developers, but they’re definitely worth all the amazing features that Kotlin offers.

1:

Java's first stable release was in 1996, while Kotlin's first stable release was around 20 years later in 2016.