1. Introduction

Kotlin provides several annotations to facilitate the compatibility of Kotlin classes with Java.

In this tutorial, we’ll specifically explore Kotlin’s JVM annotations, how we can use them, and what effect they have when we use Kotlin classes in Java.

2. Kotlin’s JVM Annotations

Kotlin’s JVM annotations affect how Kotlin code is compiled to bytecode and how the resulting classes can be used in Java.

Most of the JVM annotations don’t have an impact when we use Kotlin only. However, @JvmName and @JvmDefault also have an effect when purely using Kotlin.

3. @JvmName

We can apply the @JvmName annotation to files, functions, properties, getters, and setters.

In all cases, @JvmName defines the name of the target in the bytecode, which is also the name we can use when referring to the target from Java.

The annotation doesn’t change the name of a class, function, getter, or setter when we call it from Kotlin itself.

Let’s look at each possible target in more detail.

3.1. File Names

By default, all top-level functions and properties in a Kotlin file are compiled to filenameKt.class, and all classes are compiled to className.class.

Let’s say we have a file named message.kt, which contains top-level declarations and a class named Message:

package jvmannotation

fun getMyName() : String {
    return "myUserId"
}

class Message {
}

The compiler will create two class files: MessageKt.class and Message.class. We can now call both from Java:

Message m = new Message();
String me = MessageKt.getMyName();

If we want to give MessageKt.class a different name, we can add the @JvmName annotation in the first line of the file:

@file:JvmName("MessageHelper") 
package jvmannotation

In Java, we can now use the name which is defined in the annotation:

String me = MessageHelper.getMyName();

The annotation doesn’t change the name of the class file. It’ll remain as Message.class.

3.2. Function Names

The @JvmName annotation changes the name of a function in the bytecode. We can call the following function:

@JvmName("getMyUsername")
fun getMyName() : String {
    return "myUserId"
}

Then, from Java, we can use the name we supplied in the annotation:

String username = MessageHelper.getMyUsername();

While in Kotlin, we’ll use the actual name:

val username = getMyName()

There are two interesting use cases where @JvmName can come in handy – with functions and with type erasure.

3.3. Function Name Conflicts

The first use case is a function with the same name as an auto-generated getter or setter method.

The following code:

val sender = "me" 
fun getSender() : String = "from:$sender"

Will produce a compile-time error:

Platform declaration clash: The following declarations have the same JVM signature (getSender()Ljava/lang/String;)
public final fun <get-sender>(): String defined in jvmannotation.Message
public final fun getSender(): String defined in jvmannotation.Message

The reason for the error is that Kotlin automatically generates a getter method, and we cannot have an additional function with the same name.

If we want to have a function with that name, we can use @JvmName to tell the Kotlin compiler to rename the function at the bytecode level:

@JvmName("getSenderName")
fun getSender() : String = "from:$sender"

We can now call the function from Kotlin by the actual name and access the member variable as usual:

val formattedSender = message.getSender()
val sender = message.sender

From Java, we can call the function by the name defined in the annotation and access the member variable by the generated getter method:

String formattedSender = m.getSenderName();
String sender = m.getSender();

At this point, we might want to note that doing a getter resolution like this should be avoided as much as possible, as it might cause naming confusion.

3.4. Type Erasure Conflicts

The second use case is when a name clashes due to generic type erasure.

Here, we’ll look at a quick example. The following two methods cannot be defined within the same class, as their method signature is the same in the JVM:

fun setReceivers(receiverNames : List<String>) {
}

fun setReceivers(receiverNames : List<Int>) {
}

We’ll see a compilation error:

Platform declaration clash: The following declarations have the same JVM signature (setReceivers(Ljava/util/List;)V)

If we want to have the same name for both functions in Kotlin, we can annotate one of the functions with @JvmName:

@JvmName("setReceiverIds")
fun setReceivers(receiverNames : List<Int>) {
}

Now, we can call both functions from Kotlin with their declared name setReceivers() since Kotlin considers both signatures as different. From Java, we can call the two functions by two separate names, setReceivers() and setReceiverIds().

3.5. Getters and Setters

We can also apply the @JvmName annotation to change the names of the default getters and setters.

Let’s look at the following class definition in Kotlin:

class Message {
    val sender = "me"
    var text = ""
    private val id = 0
    var hasAttachment = true
    var isEncrypted = true
}

From Kotlin, we can refer to the class members directly; for example, we can assign a value to text:

val message = Message()
message.text = "my message"
val copy = message.text

From Java, however, we call getter and setter methods, which are auto-generated by the Kotlin compiler:

Message m = new Message();
m.setText("my message");
String copy = m.getText();

If we want to change the name of a generated getter or setter method, we can add the @JvmName annotation to the class member:

@get:JvmName("getContent")
@set:JvmName("setContent")
var text = ""

Now, we can access the text in Java by the defined getter and setter names:

Message m = new Message();
m.setContent("my message");
String copy = m.getContent();

However, the @JvmName annotation doesn’t change how we access the class member from Kotlin. We can still directly access the variable:

message.text = "my message"

In Kotlin, the following will still result in a compilation error:

m.setContent("my message");

3.6. Naming Conventions

The @JvmName annotation also comes in handy when we want to conform to certain naming conventions when calling our Kotlin class from Java.

As we’ve seen, the compiler adds the prefix get to generated getter methods. However, this is not the case for fields with names that start with is. In Java, we can access the two booleans in our message class in the following way:

Message message = new Message();
boolean isEncrypted = message.isEncrypted();
boolean hasAttachment = message.getHasAttachment();

As we can see, the compiler doesn’t prefix the getter method for isEncrypted. That seems what one would expect, as it would sound unnatural to have a getter with the getIsEncrypted().

However, that only applies to properties starting with is. We still have getHasAttachment(). Here, we can add the @JvmName annotation:

@get:JvmName("hasAttachment")
var hasAttachment = true

And we’ll get a more Java-idiomatic getter:

boolean hasAttachment = message.hasAttachment();

3.7. Access Modifier Restrictions

Note that the annotations can only be applied to class members with the appropriate access rights.

If we attempt to add @set:JvmName to an immutable member:

@set:JvmName("setSender")
val sender = "me"

We’ll get a compile-time error:

Error:(11, 5) Kotlin: '@set:' annotations could be applied only to mutable properties

And if we attempt to add @get:JvmName or @set:JvmName to a private member:

@get:JvmName("getId")
private id = 0

We’ll see only a warning:

An accessor will not be generated for 'id', so the annotation will not be written to the class file

The Kotlin compiler will then ignore the annotation and not generate any getter or setter method.

4. @JvmStatic and @JvmField

We already have two articles that describe the @JvmField and @JvmSynthetic annotation, therefore, we won’t cover those in detail here.

However, we’ll have a quick look at @JvmField to point out the differences between constants and the @JvmStatic annotation.

4.1. @JvmStatic

The @JvmStatic annotation can be applied to a function or a property of a named object or a companion object.

Let’s begin with an unannotated MessageBroker:

object MessageBroker {
    var totalMessagesSent = 0
    fun clearAllMessages() { }
}

In Kotlin, we can access these properties and functions in a static way:

val total = MessageBroker.totalMessagesSent
MessageBroker.clearAllMessages()

However, if we want to do the same in Java, we need to do so via the INSTANCE of that object:

int total = MessageBroker.INSTANCE.getTotalMessagesSent();
MessageBroker.INSTANCE.clearAllMessages();

This doesn’t look very idiomatic in Java. Therefore, we can use the @JvmStatic annotation:

object MessageBroker {
    @JvmStatic
    var totalMessagesSent = 0
    @JvmStatic
    fun clearAllMessages() { }
}

Now we see static properties and methods in Java as well:

int total = MessageBroker.getTotalMessagesSent();
MessageBroker.clearAllMessages();

4.2. @JvmField, @JvmStatic and Constants

To better understand the difference between @JvmField, @JvmStatic, and a constant in Kotlin, let’s look at the following example:

object MessageBroker {
    @JvmStatic
    var totalMessagesSent = 0

    @JvmField
    var maxMessagePerSecond = 0

    const val maxMessageLength = 0
}

A named object is the Kotlin implementation of a singleton. It’s compiled into a final class with a private constructor and a public static INSTANCE field. The Java equivalent of the above class is:

public final class MessageBroker {
    private static int totalMessagesSent = 0;
    public static int maxMessagePerSecond = 0;
    public static final int maxMessageLength = 0;
    public static MessageBroker INSTANCE = new MessageBroker();
    
    private MessageBroker() {
    }
    
    public static int getTotalMessagesSent() {
        return totalMessagesSent;
    }
    
    public static void setTotalMessagesSent(int totalMessagesSent) {
        this.totalMessagesSent = totalMessagesSent;
    }
}

We see that a property annotated with @JvmStatic is the equivalent of a private static field and corresponding getter and setter methods. A field annotated with @JvmField is the equivalent of a public static field, and a constant is the equivalent of a public static final field.

5. @JvmOverloads

In Kotlin, we can provide default values for the parameters of a function. This helps to reduce the number of necessary overloads and keeps function calls short.

Let’s look at the following named object:

object MessageBroker {
    @JvmStatic
    fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List {
        return ArrayList()
    }
}

We can call findMessages in multiple different ways by successively leaving out the parameters with default values
from right to left:

MessageBroker.findMessages("me", "text", 5);
MessageBroker.findMessages("me", "text");
MessageBroker.findMessages("me");

Note that we cannot skip the value for the first parameter sender as we do not have a default value.

However, from Java, we need to provide the values for all parameters:

MessageBroker.findMessages("me", "text", 10);

We see that, when using our Kotlin function in Java, we don’t benefit from the default parameter values but need to provide all values explicitly.

If we want to have multiple method overloads in Java as well, we can add the @JvmOverloads annotation:

@JvmStatic
@JvmOverloads
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List {
    return ArrayList()
}

The annotation instructs the Kotlin compiler to generate (n + 1) overloaded methods for n parameters with default values:

  1. One overloaded method with all parameters.
  2. One method per default parameter by successively leaving out parameters with a default value from right to left.

The Java equivalent of these functions are:

public static List<Message> findMessages(String sender, String type, int maxResults)
public static List<Message> findMessages(String sender, String type)
public static List<Message> findMessages(String sender)

Since our function has two parameters with a default value, we can now call it from Java in the same way:

MessageBroker.findMessages("me", "text", 10);
MessageBroker.findMessages("me", "text");
MessageBroker.findMessages("me");

6. @JvmDefault

In Kotlin, like in Java 8, we can define default methods for an interface:

interface Document {
    fun getType() = "document"
}

class TextDocument : Document

fun main() {
    val myDocument = TextDocument()
    println("${myDocument.getType()}")
}

This even works if we run on a Java 7 JVM. Kotlin achieves this by implementing a static inner class that implements the default method.

In this tutorial, we won’t look deeper into the generated bytecode. Instead, we’ll focus on how we can use these interfaces in Java. Furthermore, we’ll see the impact of @JvmDefault on interface delegation.

Before moving any further, we should mention that as of Kotlin 1.5, the @JvmDefault annotation has been deprecated in favor of using the new -Xjvm-default modes, which are all or all-compatibility. These modes specify that a JVM default method should be generated for non-abstract Kotlin interface members.

6.1. Kotlin Default Interface Methods and Java

Let’s look at a Java class that implements our interface:

public class HtmlDocument implements Document {
}

We’ll get a compilation error saying:

Class 'HtmlDocument' must either be declared abstract or implement abstract method 'getType()' in 'Document'

If we do this in Java 7 or below, we expect this since default interface methods were a new feature in Java 8. However, in Java 8, we expect to have the default implementation available. We can achieve this by annotating the method:

interface Document {
    @JvmDefault
    fun getType() = "document"
}

To be able to use the @JvmDefault annotation, we need to add one of the following two arguments to the Kotlin compiler:

  • Xjvm-default=enable – Only the default method of the interface is generated
  • Xjvm-default=compatibility – Both the default method and the static inner class are generated

6.2. @JvmDefault and Interface Delegation

Methods annotated with @JvmDefault are excluded from interface delegation. That means that the annotation also changes the way we can use such a method in Kotlin itself.

Let’s look at what that actually means.

The class TextDocument implements the interface Document and overrides getType():

interface Document {
    @JvmDefault
    fun getTypeDefault() = "document"

    fun getType() = "document"
}

class TextDocument : Document {
    override fun getType() = "text"
}

We can define another class which delegates the implementation to TextDocument:

class XmlDocument(d : Document) : Document by d

Both classes will use the method which is implemented in our TextDocument class:

@Test
fun testDefaultMethod() {
    val myDocument = TextDocument()
    val myTextDocument = XmlDocument(myDocument)

    assertEquals("text", myDocument.getType())
    assertEquals("text", myTextDocument.getType())
    assertEquals("document", myTextDocument.getTypeDefault())
}

We see that the method getType() of both classes returns the same value, while the method getTypeDefault(), which is annotated with @JvmDefault, returns a different value.
This is because getType() is not delegated, and as XmlDocument does not override the method, the default implementation is called.

7. @Throws

7.1. Exceptions in Kotlin

Kotlin doesn’t have checked exceptions, which means a surrounding try-catch is always optional:

fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Message> {
    if(sender.isEmpty()) {
        throw IllegalArgumentException()
    }
    return ArrayList()
}

We see that the method getType() of both classes returns the same value, and the method getTypeDefault, which is annotated with @JvmDefault, returns a different value.

We can call the function either with or without a surrounding try-catch:

MessageBroker.findMessages("me")
    
try {
    MessageBroker.findMessages("me")
} catch(e : IllegalArgumentException) {
}

If we call our Kotlin function from Java, the try-catch is also optional:

MessageBroker.findMessages("");

try {
    MessageBroker.findMessages("");
} catch (Exception e) {
    e.printStackTrace();
}

7.2. Creating Checked Exceptions for Use in Java

If we want to have a checked exception when using our function in Java, we can add the @Throws annotation:

@Throws(Exception::class)
fun findMessages(sender : String, type : String = "text", maxResults : Int = 10) : List<Message> {
    if(sender.isEmpty()) {
        throw IllegalArgumentException()
    }
    return ArrayList()
}

This annotation instructs the Kotlin compiler to create the equivalent of:

public static List<Message> findMessage(String sender, String type, int maxResult) throws Exception {
    if(sender.length() == 0) {
        throw new Exception();
    }
    return  new ArrayList<>();
}

If we now omit the try-catch in Java, we get a compile-time error:

Unhandled exception: java.lang.Exception

However, if we use the function in Kotlin, we can still omit the try-catch, as the annotation only changes the way that it’s called from Java.

8. @JvmWildcard and @JvmSuppressWildcards

8.1. Generic Wildcards

In Java, we need wildcards to handle generics in combination with inheritance. Even though Integer extends Number, the following assignment leads to a compilation error:

List<Number> numberList = new ArrayList<Integer>();

We can solve the problem by using a wildcard:

List<? extends Number> numberList = new ArrayList<Integer>();

In Kotlin, there are no wildcards, and we can simply write:

val numberList : List<Number> = ArrayList<Int>()

This leads to the question of what happens if we use a Kotlin class that contains such a list.

As an example, let’s look at a function that takes a list as a parameter:

fun transformList(list : List<Number>) : List<Number>

In Kotlin, we can call this function with any list whose parameter extends Number:

val list = transformList(ArrayList<Long>())

Of course, if we want to call this function from Java, we expect this to be possible as well. This indeed works, since from a Java perspective, the function looks like this:

public List<Number> transformList(List<? extends Number> list)

The Kotlin compiler implicitly created a function with a wildcard.

Let’s see when this happens and when not.

8.2. Kotlin’s Wildcard Rule

Here, the basic rule is that by default, Kotlin only produces a wildcard where necessary.

If the type parameters is a final class, there is no wildcard:

fun transformList(list : List<String>) // Kotlin
public void transformList(List<String> list) // Java

Here, there is no need for “*? extends Number*” because no class can extend String. However, if the class can be extended, we’ll have a wildcard. Number is not a final class, so we’ll have:

fun transformList(list : List<Number>) // Kotlin
public void transformList(List<? extends Number> list) // Java

Furthermore, return types don’t have a wildcard:

fun transformList() : List<Number> // Kotlin 
public List<Number> transformList() // Java

8.3. Wildcard Configuration

However, there might be situations where we want to change the default behavior. To do so, we can use the JVM annotations. JvmWildcard ensures that the annotated type parameter always gets a wildcard. And JvmSuppressWildcards ensures that it won’t get a wildcard.

Let’s annotate the above function:

fun transformList(list : List<@JvmSuppressWildcards Number>) : List<@JvmWildcard Number>

Now let’s look at the method signature as seen from Java, which shows the effect of the annotations:

public List<? extends Number> transformListInverseWildcards(List<Number> list)

Finally, we should note that wildcards in return types are generally bad practices in Java; however, there might be a situation where we need them. Then, the Kotlin JVM annotations come in handy.

9. @JvmMultifileClass

We already saw how we can apply the @JvmName annotation to a file in order to define the name of the class where all top-level declarations are compiled. Of course, the name we provide has to be unique.

Suppose we have two Kotlin files in the same package, both with the @JvmName annotation and the same target class name. The first file MessageConverter.kt, has the following code:

@file:JvmName("MessageHelper")
package jvmannotation
convert(message: Message) = // conversion code

And the second file Message.kt with the following code:

@file:JvmName("MessageHelper") 
package jvmannotation
fun archiveMessage() =  // archiving code

If we do this, we’ll get an error:

// Error:(1, 1) Kotlin: Duplicate JVM class name 'jvmannotation/MessageHelper' 
//  generated from: package-fragment jvmannotation, package-fragment jvmannotation

This is because the Kotlin compiler attempts to create two classes with the same name.

If we want to combine all top-level declarations of both files in one single class with the name MessageHelper.class, we can add the @JvmMultifileClass to both files.

Let’s add @JvmMultifileClass to MessageConverter.kt:

@file:JvmName("MessageHelper")
@file:JvmMultifileClass
package jvmannotationfun 
convert(message: Message) = // conversion code

And then, we’ll add it to Message.kt as well:

@file:JvmName("MessageHelper") 
@file:JvmMultifileClass
package jvmannotation
fun archiveMessage() =  // archiving code

In Java, we can see all top-level declarations from both Kotlin files are now unified into MessageHelper:

MessageHelper.archiveMessage();
MessageHelper.convert(new Message());

The annotation does not affect how we call the functions from Kotlin.

10. @JvmPackageName

All JVM platform annotations are defined in the package kotlin.jvm. When we look at this package, we notice that there’s another annotation: @JvmPackageName.

This annotation can change the package name much like @file:JvmName changes the name of the generated class file.

However, the annotation is marked as internal, which means that it cannot be used outside the Kotlin library classes. Therefore, we won’t look into more detail in this article.

11. Annotation Target Cheat Sheet

A good source to find all the information about the JVM annotations available in Kotlin is the official documentation. Another good place to find all the details is the code itself. The definitions (including JavaDoc) can be found in the package kotlin.jvm in kotlin-stdlib.jar.

The following table summarizes which annotations can be used with which target:

kotlin jmv anns

12. Conclusion

In this article, we had a look at Kotlin’s JVM annotations. The full source code for the examples is available on GitHub.


« 上一篇: RxKotlin介绍