Guia de anotações da plataforma JVM no Kotlin

Guia de anotações da plataforma JVM no Kotlin

1. Introdução

O Kotlin fornece várias anotações para facilitar a compatibilidade das classes do Kotlin com o Java.

Neste tutorial, vamos explorar especificamente as anotações JVM do Kotlin, como podemos usá-las e que efeito elas têm quando usamos as classes Kotlin em Java.

2. Anotações JVM de Kotlin

As anotações JVM de Kotlin afetam a maneira como o código Kotlin é compilado para bytecode e como as classes resultantes podem ser usadas em Java.

A maioria das anotações JVM não tem impacto quando usamos apenas Kotlin. No entanto,@JvmName e@JvmDefault também têm um efeito ao usar apenas Kotlin.

3. @JvmName

Podemos aplicar a anotação@JvmName a arquivos, funções, propriedades, getters e setters.

Em todos os casos,@JvmName define o nome do destino no bytecode, que também é o nome que podemos usar quando nos referimos ao destino de Java.

A anotação não altera o nome de uma classe, função, getter ou setter quando o chamamos do próprio Kotlin.

Vamos examinar cada alvo possível com mais detalhes.

3.1. Nomes de arquivo

Por padrão, todas as funções e propriedades de nível superior em um arquivo Kotlin são compiladas emfilenameKt.classe todas as classes são compiladas emclassName.class.

Digamos que temos um arquivo chamadomessage.kt, que contém declarações de nível superior e uma classe chamadaMessage:

package jvmannotation

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

class Message {
}

O compilador criará dois arquivos de classe:MessageKt.classeMessage.class. Agora podemos chamar ambos de Java:

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

Se quisermos dar aMessageKt.class um nome diferente,we can add the @JvmName annotation in the first line of the file:

@file:JvmName("MessageHelper")
package jvmannotation

Em Java, agora podemos usar o nome definido na anotação:

String me = MessageHelper.getMyName();

A anotação não altera o nome do arquivo da classe. Ele permanecerá comoMessage.class.

3.2. Nomes de Função

The @JvmName annotation changes the name of a function in the bytecode. Podemos chamar a seguinte função:

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

E, a partir de Java, podemos usar o nome que fornecemos na anotação:

String username = MessageHelper.getMyUsername();

Enquanto estiver em Kotlin, usaremos o nome real:

val username = getMyName()

Existem dois casos de uso interessantes onde@JvmName pode ser útil - com funções e com eliminação de tipo.

3.3. Conflitos no nome da função

O primeiro caso de uso é uma função com o mesmo nome de um método getter ou setter gerado automaticamente.

O código a seguir:

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

Irá produzir um erro em tempo de compilação:

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

O motivo do erro é que o Kotlin gera automaticamente um método getter e não podemos ter uma função adicional com o mesmo nome.

Se quisermos ter uma função com esse nome, podemos usar@JvmName para dizer ao compilador Kotlin para renomear a função no nível do bytecode:

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

Agora podemos chamar a função do Kotlin pelo nome real e acessar a variável de membro como de costume:

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

Em Java, podemos chamar a função pelo nome definido na anotação e acessar a variável de membro pelo método getter gerado:

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

Nesse ponto, podemos observar que a resolução de um getter como essa deve ser evitada o máximo possível, pois isso pode causar confusões de nomes.

3.4. Tipo Conflitos de apagamento

O segundo caso de uso é quando um nome entra em conflito devido atype erasure genérico.

Aqui, veremos um exemplo rápido. Os dois métodos a seguir não podem ser definidos na mesma classe, pois a assinatura do método é a mesma na JVM:

fun setReceivers(receiverNames : List) {
}

fun setReceivers(receiverNames : List) {
}

Veremos um erro de compilação:

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

Se quisermos ter o mesmo nome para ambas as funções no Kotlin, podemos anotar uma das funções com@JvmName:

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

Agora podemos chamar as duas funções do Kotlin com seus nomes declaradossetReceivers(), pois o Kotlin considera as duas assinaturas diferentes. Em Java, podemos chamar as duas funções por dois nomes separados,setReceivers()esetReceiverIds().

3.5. Getters e Setters

Também podemos aplicar a anotação@JvmName achange the names of the default getters and setters.

Vejamos a seguinte definição de classe em Kotlin:

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

Em Kotlin, podemos nos referir aos membros da classe diretamente, por exemplo, podemos atribuir um valor atext:

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

No Java, no entanto, chamamos métodos getter e setter que são gerados automaticamente pelo compilador Kotlin:

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

Se quisermos alterar o nome de um método getter ou setter gerado, podemos adicionar a anotação@JvmName ao membro da classe:

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

Agora, podemos acessar o texto em Java pelos nomes getter e setter definidos:

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

No entanto, a anotação@JvmName não muda a forma como acessamos o membro da classe de Kotlin. Ainda podemos acessar diretamente a variável:

message.text = "my message"

No Kotlin, o seguinte ainda resultará em um erro de compilação:

m.setContent("my message");

3.6. Convenções de nomenclatura

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

Como vimos, o compilador adiciona o prefixoget aos métodos getter gerados. No entanto, esse não é o caso para campos com nomes que começam comis. Em Java, podemos acessar os doisbooleans em nossa classe de mensagem da seguinte maneira:

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

Como podemos ver, o compilador não prefixa o método getter paraisEncrypted.. Isso parece o que se esperaria, pois não seria natural ter um getter comgetIsEncrypted().

No entanto, isso se aplica apenas a propriedades começando comis. Ainda temosgetHasAttachment(). Aqui, podemos adicionar a anotação@JvmName:

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

E obteremos um getter mais idiomático do Java:

boolean hasAttachment = message.hasAttachment();

3.7. Restrições do modificador de acesso

Observe que a varredura de anotações ó pode ser aplicada a membros da classe com os direitos de acesso apropriados.

Se tentarmos adicionar@set:JvmName a um membro imutável:

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

Obteremos um erro de tempo de compilação:

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

E se tentarmos adicionar@get:JvmName ou@set:JvmName a um membro privado:

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

Veremos apenas um aviso:

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

E o compilador Kotlin ignorará a anotação e não gerará nenhum método getter ou setter.

4. @JvmStatic e @JvmField

Já temos dois artigos que descrevem as anotações de@JvmField e@JvmSynthetic, portanto, não os cobriremos em detalhes aqui.

No entanto, daremos uma olhada rápida em@JvmField para apontar as diferenças entre as constantes e a anotação@JvmStatic.

4.1. @JvmStatic

A anotação@JvmStatic pode ser aplicada a uma função ou propriedade de um objeto nomeado ou um objeto companheiro.

Vamos começar com umMessageBroker não anotado:

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

No Kotlin, podemos acessar essas propriedades e funções de maneira estática:

val total = MessageBroker.totalMessagesSent
MessageBroker.clearAllMessages()

No entanto, se queremos fazer o mesmo em Java, precisamos fazê-lo através da INSTANCE desse objeto:

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

Isso não parece muito idiomático em Java. Portanto, podemos usar a anotação@JvmStatic:

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

Agora também vemos propriedades e métodos estáticos em Java:

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

4.2. @JvmField, @JvmStatic e constantes

Para entender melhor a diferença entre@JvmField,@JvmStatic e aconstant em Kotlin, vejamos o seguinte exemplo:

object MessageBroker {
    @JvmStatic
    var totalMessagesSent = 0

    @JvmField
    var maxMessagePerSecond = 0

    const val maxMessageLength = 0
}

Um objeto nomeado é a implementação Kotlin de um singleton. Ele é compilado para uma classefinal com um construtor privado e um campopublicstaticINSTANCE. O equivalente em Java da classe acima é:

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;
    }
}

Vemos que uma propriedade anotada com@JvmStatic é o equivalente a um campoprivate static e os métodos getter e setter correspondentes. Um campo anotado com@JvmField é o equivalente a um campopublic static e uma constante é o equivalente a um campopublic static final.

5. @JvmOverloads

In Kotlin, we can provide default values for the parameters of a function.  Isso ajuda a reduzir o número de sobrecargas necessárias e mantém as chamadas de função curtas.

Vejamos o seguinte objeto nomeado:

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

Podemos chamarfindMessages de várias maneiras diferentes, deixando de fora sucessivamente os parâmetros com valores padrão da direita para a esquerda:

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

Observe que não podemos ignorar o valor do primeiro parâmetrosender, pois não temos um valor padrão.

No entanto, no Java, precisamos fornecer os valores para todos os parâmetros:

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

Vemos que, ao usar nossa função Kotlin em Java, não nos beneficiamos do  valores de parâmetro padrão, mas precisa fornecer todos os valores explicitamente.

Se quisermos ter várias sobrecargas de método em Java também, podemos adicionar a anotação@JvmOverloads:

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

A anotação instrui o compilador Kotlin a gerar métodos sobrecarregados de(n + 1) para  Parâmetrosn com valores padrão:

  1. Um método sobrecarregado com todos os parâmetros.

  2. Um método por parâmetro padrão, excluindo sucessivamente parâmetros com um valor padrão da direita para a esquerda.

O equivalente em Java dessas funções são:

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

Como nossa função possui dois parâmetros com um valor padrão, agora podemos chamá-la de Java da mesma maneira:

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

6. @JvmDefault

No Kotlin, como no Java 8, podemos definir métodos padrão para uma interface:

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

class TextDocument : Document

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

Isso funciona mesmo se rodarmos em uma Java 7 JVM. Kotlin consegue isso implementando uma classe interna estática que implementa o método padrão.

Neste tutorial, não vamos nos aprofundar no bytecode gerado. Em vez disso, vamos nos concentrar em como podemosuse these interfaces in Java. Além disso, veremosthe impact of @JvmDefault on interface delegation.

6.1. Métodos de interface padrão do Kotlin e Java

Vejamos uma classe Java que implementa nossa interface:

public class HtmlDocument implements Document {
}

Obteremos um erro de compilação, dizendo:

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

Se fizermos isso no Java 7 ou abaixo, esperamos isso, já que os métodos de interface padrão eram um novo recurso no Java 8. No entanto, no Java 8, esperamos ter a implementação padrão disponível. Podemos conseguir isso anotando o método:

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

Para poder usar a anotação@JvmDefault, precisamos adicionar um dos dois argumentos a seguir ao compilador Kotlin:

  • Xjvm-default=enable – Apenas o método padrão da interface é gerado

  • Xjvm-default=compatibility – Tanto o método padrão quanto a classe interna estática são gerados

6.2. @JvmDefaulte Delegação de interface

Os métodos anotados com@JvmDefault são excluídos da delegação de interface. Isso significa que a anotação também muda a maneira como podemos usar esse método no próprio Kotlin.

Vamos ver o que isso realmente significa.

A classeTextDocument implementa a interfaceDocumente substituigetType():

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

    fun getType() = "document"
}

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

Podemos definir outra classe, que delega a implementação paraTextDocument:

class XmlDocument(d : Document) : Document by d

Ambas as classes usarão o método que é implementado em nossa classeTextDocument:

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

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

Vemos que o métodogetType() de ambas as classes retorna o mesmo valor, enquanto o métodogetTypeDefault(), que é anotado com@JvmDefault, retorna um valor diferente. Isso ocorre porquegetType() is not delegatedeXmlDocument não substitui o método, a implementação padrão é chamada.

7. @Throws

7.1. Exceções em Kotlin

Kotlin não tem exceções verificadas, o que significa que umtry-catch circundante é sempre opcional:

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

Vimos que o método getType () de ambas as classes retorna o mesmo valor e o método getTypeDefault, que é anotado com @JvmDefault, retorna um valor diferente.

Podemos chamar a função com ou semtry-catch circundante:

MessageBroker.findMessages("me")

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

Se chamarmos nossa função Kotlin do Java, otry-catch também é opcional:

MessageBroker.findMessages("");

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

7.2. Criando exceções verificadas para uso em Java

Se quisermos ter uma exceção verificada ao usar nossa função em Java, podemos adicionar a anotação@Throws:

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

Esta anotação instrui o compilador Kotlin a criar o equivalente a:

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

Se agora omitirmostry-catch em Java, obteremos um erro de tempo de compilação:

Unhandled exception: java.lang.Exception

No entanto, se usarmos a função em Kotlin, ainda podemos omitirtry-catch, já que a anotação apenas altera a maneira como é chamada do Java.

8. @JvmWildcard e@JvmSuppressWildcards

8.1. Curingas genéricos

Em Java, precisamos de curingas para manipular genéricos em combinação com herança. EmboraInteger estendaNumber, a seguinte atribuição leva a um erro de compilação:

List numberList = new ArrayList();

Podemos resolver o problema usando um curinga:

List numberList = new ArrayList();

No Kotlin, não há curingas e podemos simplesmente escrever:

val numberList : List = ArrayList()

Isso leva à questão dewhat happens if we use a Kotlin class which contains such a list.

Como exemplo, vejamos uma função que leva uma lista como parâmetro:

fun transformList(list : List) : List

No Kotlin, podemos chamar essa função com qualquer lista cujo parâmetro estendaNumber:

val list = transformList(ArrayList())

Obviamente, se queremos chamar essa função do Java, esperamos que isso seja possível também. Isso realmente funciona, pois da perspectiva de Java, a função se parece com isso:

public List transformList(List list)

O compilador Kotlin criou implicitamente uma função com um curinga.

Vamos ver quando isso acontece e quando não.

8.2. Regra curinga de Kotlin

Aqui, a regra básica é que, por padrão, o Kotlin produz apenas um curinga quando necessário.

Se os parâmetros de tipo são uma classe final, não há curinga:

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

Aqui, não há necessidade de “? extends Number“, porque nenhuma classe pode estenderString. No entanto, se a classe puder ser estendida, teremos um curinga. Number não é uma classefinal, então teremos:

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

Além disso, os tipos de retorno não têm um caractere curinga:

fun transformList() : List // Kotlin
public List transformList() // Java

8.3. Configuração curinga

However, there might be situations, where we want to change the default behavior. Para fazer isso, podemos usar as anotações JVM. JvmWildcard garante que o parâmetro de tipo anotado sempre receba um curinga. EJvmSuppressWildcards garante que não receberá um caractere curinga.

Vamos anotar a função acima:

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

E observe a assinatura do método como vista em Java, que mostra o efeito das anotações:

public List transformListInverseWildcards(List list)

Finally, we should note that wildcards in return types are generally bad practice in Java, however, there might be a situation where we need them. Então, as anotações Kotlin JVM são úteis.

9. @JvmMultifileClass

Já vimos como podemos aplicar a anotação@JvmName a um arquivo para definir o nome da classe onde todas as declarações de nível superior são compiladas. Claro,the name we provide has to be unique.

Suponha que temos dois arquivos Kotlin no mesmo pacote, ambos com a anotação@JvmName e o mesmo nome de classe de destino. O primeiro arquivoMessageConverter.kt com o seguinte código:

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

E o segundo arquivoMessage.kt com o seguinte código:

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

Se fizermos isso, obteremos um erro:

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

Isso ocorre porque o compilador Kotlin tenta criar duas classes com o mesmo nome.

Se quisermos combinar todas as declarações de nível superior de ambos os arquivos em uma única classe com o nomeMessageHelper.class, podemos adicionar@JvmMultifileClass a ambos os arquivos.

Vamos adicionar@JvmMultifileClass aMessageConverter.kt:

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

E então, vamos adicioná-lo aMessage.kt também:

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

Em Java, podemos ver que todas as declarações de nível superior de ambos os arquivos Kotlin agora estão unificadas emMessageHelper:

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

A anotação não afeta a maneira como chamamos as funções do Kotlin.

10. @JvmPackageName

Todas as anotações da plataforma JVM são definidas no pacotekotlin.jvm. Quando olhamos para este pacote, notamos que há outra anotação:@JvmPackageName.

Esta anotação pode alterar o nome do pacote da mesma forma que@file:JvmName altera o nome do arquivo de classe gerado.

No entanto, a anotação é marcada como interna, o que significa que não pode ser usada fora das classes da biblioteca Kotlin. Portanto, não examinaremos mais detalhes neste artigo.

11. Folha de referência do alvo da anotação

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. As definições (incluindo JavaDoc) podem ser encontradas no pacotekotlin.jvm emkotlin-stdlib.jar.

A tabela a seguir resume quais anotações podem ser usadas com qual destino:

image

12. Conclusão

Neste artigo, demos uma olhada nas anotações JVM de Kotlin. O código-fonte completo dos exemplos está disponívelover on GitHub.