article banner

Kotlin and Java interoperability: Useful annotations

This is a chapter from the book Advanced Kotlin. You can find it on LeanPub or Amazon.

JVM has some platform limitations that are a result of its implementation. Consider the following two functions:

fun List<Long>.average() = sum().toDouble() / size fun List<Int>.average() = sum().toDouble() / size

Each of these functions is an extension on List with a different type argument. This is perfectly fine for Kotlin, but not when targeted for the JVM platform. Due to type erasure, both these functions on JVM are considered methods called average with a single parameter of type List. So, having them both defined in the same file leads to a platform name clash.

A simple solution to this problem is to use the JvmName annotation, which changes the name that will be used for this function under the hood on the JVM platform. Usage will not change in Kotlin, but if you want to use these functions from a non-Kotlin JVM language, you need to use the names specified in the annotation.

@JvmName("averageLongList") fun List<Long>.average() = sum().toDouble() / size @JvmName("averageIntList") fun List<Int>.average() = sum().toDouble() / size fun main() { val ints: List<Int> = List(10) { it } println(ints.average()) // 4.5 val longs: List<Long> = List(10) { it.toLong() } println(longs.average()) // 4.5 }
// Java
public class JavaClass {
    public static void main(String[] args) {
        List<Integer> ints = List.of(1, 2, 3);
        double res1 = TestKt.averageIntList(ints);
        System.out.println(res1); // 2.0
        List<Long> longs = List.of(1L, 2L, 3L);
        double res2 = TestKt.averageLongList(longs);
        System.out.println(res2); // 2.0
    }
}

The JvmName annotation is useful for resolving name conflicts, but there is another case where it is used much more often. As I explained in the Kotlin Essentials book, for all Kotlin top-level functions and properties on JVM, a class is generated whose name is the file name with the "Kt" suffix. Top-level functions and properties are compiled to static JVM functions in this class and thus can be used from Java.

package test const val e = 2.71 fun add(a: Int, b: Int) = a + b
// Compiles to the analog of the following Java code
package test;

public final class TestKt {
    public static final double e = 2.71;
    
    public static final int add(int a, int b) {
        return a + b;
    }
}
// Usage from Java
public class JavaClass {
    public static void main(String[] args) {
        System.out.println(TestKt.e); // 2.71
        int res = TestKt.add(1, 2);
        System.out.println(res); // 3
    }
}

This auto-generated name is not always what we want to use. Often we would prefer to specify a custom name, in which case we should use the JvmName annotation for the file. As I explained in the Annotation targets section, file annotations must be placed at the beginning of the file, even before the package definition, and they must use the file target. The name that we will specify in the JvmName file annotation will be used for the class that stores all the top-level functions and properties.

@file:JvmName("Math") package test const val e = 2.71 fun add(a: Int, b: Int) = a + b
// Compiles to the analog of the following Java code
package test;

public final class Math {
    public static final double e = 2.71;
    
    public static final int add(int a, int b) {
        return a + b;
    }
}
// Usage from Java
public class JavaClass {
    public static void main(String[] args) {
        System.out.println(Math.e); // 2.71
        int res = Math.add(1, 2);
        System.out.println(res); // 3
    }
}

JvmMultifileClass

Because all functions or fields on JVM must be located in a class, it is common practice in Java projects to make huge classes like Math or Collections that are holders for static elements. In Kotlin, we use top-level functions instead, which offers us the convenience that we don’t need to collect all these functions and properties in the same file. However, when we design Kotlin code for use from Java, we might want to collect elements from multiple files that define the same package in the same generated class. For that, we need to use the JvmMultifileClass annotation next to JvmName.

// FooUtils.kt @file:JvmName("Utils") @file:JvmMultifileClass package demo fun foo() { // ... }
// BarUtils.kt @file:JvmName("Utils") @file:JvmMultifileClass package demo fun bar() { // ... }
// Usage from Java

import demo.Utils;

public class JavaClass {
    public static void main(String[] args) {
        Utils.foo();
        Utils.bar();
    }
}

JvmOverloads

Another inconsistency between Java and Kotlin is a result of another Java limitation. Java, unlike most modern programming languages, does not support named optional arguments, therefore default Kotlin arguments cannot be used in Java.

class Pizza( val tomatoSauce: Int = 1, val cheese: Int = 0, val ham: Int = 0, val onion: Int = 0, ) class EmailSender { fun send( receiver: String, title: String = "", message: String = "", ) { /*...*/ } }

Note that when all constructor parameters are optional, two constructors are generated: one with all the parameters, and one that uses only default values.

The best that can be offered for Java is an implementation of the telescoping constructor pattern so that Kotlin can generate different variants of a constructor or function for a different number of arguments. This is not done by default in order to avoid generating methods that are not used. So, you need to use the JvmOverloads annotation before a function to make Kotlin generate different variants with different numbers of expected arguments.

class Pizza @JvmOverloads constructor( val tomatoSauce: Int = 1, val cheese: Int = 0, val ham: Int = 0, val onion: Int = 0, ) class EmailSender { @JvmOverloads fun send( receiver: String, title: String = "", message: String = "", ) { /*...*/ } }

Notice that Kotlin does not generate all possible combinations of parameters; it only generates one additional variant for each optional parameter.

Do not confuse JvmOverloads with JVM overlords.

1:

A companion object is also an object declaration, as I explained in the Kotlin Essentials book.