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:
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.
// 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.
// 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.
// 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
.
// 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.
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.
Notice that Kotlin does not generate all possible combinations of parameters; it only generates one additional variant for each optional parameter.
A companion object is also an object declaration, as I explained in the Kotlin Essentials book.