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.
publicclassJavaClass{// IOException are checked exceptions,// and they must be declared with throwsStringreadFirstLine(String fileName)throwsIOException{FileInputStream fis =newFileInputStream(fileName);InputStreamReader reader =newInputStreamReader(fis);BufferedReader bufferedReader =newBufferedReader(reader);return bufferedReader.readLine();}voidcheckFirstLine(){String line;try{ line =readFirstLine("number.txt");// We must catch checked exceptions, // or declare them with throws}catch(IOException e){thrownewRuntimeException(e);}// parseInt throws NumberFormatException,// which is an unchecked exceptionint number =Integer.parseInt(line);// Dividing two numbers might throw// ArithmeticException of number is 0,// which is an unchecked exceptionSystem.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 centuries[^54_1], 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.
[^54_1]: Java's first stable release was in 1996, while Kotlin's first stable release was around 20 years later in 2016.
Marcin Moskala is a highly experienced developer and Kotlin instructor as the founder of Kt. Academy, an official JetBrains partner specializing in Kotlin training, Google Developers Expert, known for his significant contributions to the Kotlin community. Moskala is the author of several widely recognized books, including "Effective Kotlin," "Kotlin Coroutines," "Functional Kotlin," "Advanced Kotlin," "Kotlin Essentials," and "Android Development with Kotlin."
Beyond his literary achievements, Moskala is the author of the largest Medium publication dedicated to Kotlin. As a respected speaker, he has been invited to share his insights at numerous programming conferences, including events such as Droidcon and the prestigious Kotlin Conf, the premier conference dedicated to the Kotlin programming language.
Software architect with 15 years of experience, currently working on building infrastructure for AI. I think Kotlin is one of the best programming languages ever created.
Owen has been developing software since the mid 1990s and remembers the productivity of languages such as Clipper and Borland Delphi.
Since 2001, He moved to Web, Server based Java and the Open Source revolution.
With many years of commercial Java experience, He picked up on Kotlin in early 2015.
After taking detours into Clojure and Scala, like Goldilocks, He thinks Kotlin is just right and tastes the best.
Owen enthusiastically helps Kotlin developers continue to succeed.