Guia de interoperabilidade entre Kotlin e Java

Este documento é composto por um conjunto de regras para a criação de APIs públicas em Java e Kotlin para que o código se torne idiomático quando consumido a partir de outra linguagem.

Última atualização: 18/05/2018

Java (para consumo de Kotlin)

Nenhuma palavra-chave específica

Não use nenhuma das palavras-chave específicas do Kotlin, como nome de métodos ou campos. Eles requerem o uso de crase para escape quando chamados no Kotlin. Palavras-chave não específicas, palavras-chave modificadoras e identificadores especiais são permitidos (links em inglês).

Por exemplo, a função when do Mockito requer crase quando usada no Kotlin:

val callable = Mockito.mock(Callable::class.java)
Mockito.`when`(callable.call()).thenReturn(/* … */)

Evitar nomes de extensão Any

Evite usar os nomes das funções de extensão em Any para métodos ou os nomes das propriedades de extensão em Any para campos, a menos que seja absolutamente necessário (links em inglês). Embora os métodos e campos dos membros sempre tenham precedência sobre as funções ou propriedades de extensão de Any, pode ser difícil saber qual deles está sendo chamado ao ler o código.

Anotações de nulidade

Cada parâmetro, retorno e tipo de campo não primitivo em uma API pública precisa ter uma anotação de nulidade. Os tipos não anotados são interpretados como tipos "plataforma", que têm nulidade ambígua (link em inglês).

Por padrão, as sinalizações do compilador Kotlin respeitam as anotações JSR 305, mas as sinalizam com avisos. Também é possível definir uma sinalização para que o compilador trate as anotações como erros.

Parâmetros lambda por último

Os tipos de parâmetro qualificados para conversão de SAM precisam ser os últimos (link em inglês).

Por exemplo, a assinatura do método RxJava 2’s Flowable.create() é definida como:

public static  Flowable create(
    FlowableOnSubscribe source,
    BackpressureStrategy mode) { /* … */ }

Como FlowableOnSubscribe está qualificado para a conversão de SAM, as chamadas de função desse método no Kotlin têm esta aparência:

Flowable.create({ /* … */ }, BackpressureStrategy.LATEST)

No entanto, se os parâmetros fossem invertidos na assinatura do método, as chamadas de função poderiam usar a sintaxe de lambda no final:

Flowable.create(BackpressureStrategy.LATEST) { /* … */ }

Prefixos de propriedade

Para que um método seja representado como uma propriedade no Kotlin, o prefixo restrito de tipo “bean” precisa ser usado.

Os métodos de acesso exigem um prefixo "get" ou, para métodos de retorno de booleano, um prefixo "is" pode ser usado.

public final class User {
  public String getName() { /* … */ }
  public boolean isActive() { /* … */ }
}
val name = user.name // Invokes user.getName()
val active = user.isActive // Invokes user.isActive()

Os métodos mutator associados exigem um prefixo "set".

public final class User {
  public String getName() { /* … */ }
  public void setName(String name) { /* … */ }
  public boolean isActive() { /* … */ }
  public void setActive(boolean active) { /* … */ }
}
user.name = "Bob" // Invokes user.setName(String)
user.isActive = true // Invokes user.setActive(boolean)

Se você quiser que os métodos sejam exibidos como propriedades, não use prefixos diferentes do padrão, como os métodos acessor prefixados ‘has’/’set’ ou non-‘get’. Métodos com prefixos fora do padrão ainda podem ser chamados como funções que podem ser aceitáveis dependendo do comportamento do método.

Sobrecarga do operador

Tenha cuidado com os nomes de método que permitem sintaxe especial do local da chamada, ou seja, sobrecarga do operador (link em inglês) no Kotlin. Os nomes de métodos como esses precisam ser usados com a sintaxe abreviada.

public final class IntBox {
  private final int value;
  public IntBox(int value) {
    this.value = value;
  }
  public IntBox plus(IntBox other) {
    return new IntBox(value + other.value);
  }
}
val one = IntBox(1)
val two = IntBox(2)
val three = one + two // Invokes one.plus(two)

Kotlin (para consumo de Java)

Nome do arquivo

Quando um arquivo contém funções ou propriedades de nível superior, sempre anote com @file:JvmName("Foo") para fornecer um bom nome.

Por padrão, os membros de nível superior em um arquivo MyClass.kt acabam em uma classe chamada MyClassKt, o que não é atraente e vaza a linguagem como um detalhe de implementação.

Considere adicionar @file:JvmMultifileClass para combinar os membros de nível superior de vários arquivos em uma única classe.

Argumentos lambda

As interfaces de método único (SAM, na sigla em inglês) definidas em Java podem ser implementadas em Kotlin e Java usando a sintaxe lambda, que alinha a implementação de forma idiomática. O Kotlin tem várias opções para definir essas interfaces, cada uma com uma pequena diferença.

Definição preferencial

As funções de ordem superior que precisam ser usadas em Java não podem usar tipos de função que retornam Unit, porque isso exigiria que os autores de chamadas do Java retornassem Unit.INSTANCE. Em vez de inserir o tipo de função na assinatura, use interfaces funcionais (SAM). Considere também o uso de interfaces funcionais (SAM, na sigla em inglês) em vez de interfaces normais ao definir interfaces que precisam ser usadas como lambdas, o que permite o uso idiomático do Kotlin.

Confira esta definição do Kotlin:

fun interface GreeterCallback {
  fun greetName(String name)
}

fun sayHi(greeter: GreeterCallback) = /* … */

Quando invocado pelo Kotlin:

sayHi { println("Hello, $it!") }

Quando invocado do Java:

sayHi(name -> System.out.println("Hello, " + name + "!"));

Mesmo quando o tipo de função não retorna uma Unit, ainda pode ser uma boa ideia transformá-la em uma interface nomeada para permitir que os autores da chamada a implementem com uma classe nomeada, e não apenas com lambdas (no Kotlin e no Java).

class MyGreeterCallback : GreeterCallback {
  override fun greetName(name: String) {
    println("Hello, $name!");
  }
}

Evitar tipos de função que retornam Unit

Confira esta definição do Kotlin:

fun sayHi(greeter: (String) -> Unit) = /* … */

Ela exige que os autores das chamadas Java retornem Unit.INSTANCE:

sayHi(name -> {
  System.out.println("Hello, " + name + "!");
  return Unit.INSTANCE;
});

Evite interfaces funcionais quando a implementação precisa ter o estado.

Quando a implementação da interface precisa ter um estado, o uso da sintaxe lambda não faz sentido. Comparável é um exemplo proeminente, porque se destina a comparar this com other, e lambdas não têm this. Não prefixar a interface com fun força o autor da chamada a usar a sintaxe object : ..., que permite que ele tenha um estado, fornecendo uma dica para o autor da chamada.

Confira esta definição do Kotlin:

// No "fun" prefix.
interface Counter {
  fun increment()
}

Ela impede a sintaxe lambda no Kotlin, exigindo esta versão mais longa:

runCounter(object : Counter {
  private var increments = 0 // State

  override fun increment() {
    increments++
  }
})

Evitar Nothing genérico

Um tipo cujo parâmetro genérico é Nothing é exposto como tipos brutos para Java. Os tipos brutos raramente são usados em Java e precisam ser evitados.

Exceções de documentos

As funções que podem lançar exceções verificadas precisam documentá-las com @Throws. Exceções de tempo de execução precisam ser documentadas no KDoc.

Tenha cuidado com as APIs para as quais uma função delega porque elas podem gerar exceções verificadas que, de outra forma, o Kotlin permite propagar silenciosamente.

Cópias defensivas

Ao retornar coleções de somente leitura sem dono ou compartilhadas em APIs public, una-as em um contêiner inalterável ou faça uma cópia defensiva. Apesar de o Kotlin impor as próprias propriedades de somente leitura, não existe tal aplicação no lado do Java. Sem o wrapper ou a cópia defensiva, as invariantes podem ser violadas retornando uma referência de coleção de vida longa.

Funções complementares

As funções públicas em um objeto complementar precisam ser anotadas com @JvmStatic para serem expostas como um método estático.

Sem a anotação, essas funções só estarão disponíveis como métodos de instância em um campo estático Companion.

Incorreto: sem anotações

class KotlinClass {
    companion object {
        fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.Companion.doWork();
    }
}

Correto: anotação @JvmStatic

class KotlinClass {
    companion object {
        @JvmStatic fun doWork() {
            /* … */
        }
    }
}
public final class JavaClass {
    public static void main(String... args) {
        KotlinClass.doWork();
    }
}

Constantes complementares

Propriedades públicas, não const, que são constantes efetivas em um companion object precisam ser anotadas com @JvmField para serem expostas como um campo estático.

Sem a anotação, essas propriedades estarão disponíveis apenas como instâncias de nome estranho "getters" no campo Companion estático. O uso de @JvmStatic em vez de @JvmField move os "getters" com um nome estranho para métodos estáticos na classe, o que ainda está incorreto.

Incorreto: sem anotações

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.Companion.getBIG_INTEGER_ONE());
    }
}

Incorreto: anotação @JvmStatic

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmStatic val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.getBIG_INTEGER_ONE());
    }
}

Correto: anotação @JvmField

class KotlinClass {
    companion object {
        const val INTEGER_ONE = 1
        @JvmField val BIG_INTEGER_ONE = BigInteger.ONE
    }
}
public final class JavaClass {
    public static void main(String... args) {
        System.out.println(KotlinClass.INTEGER_ONE);
        System.out.println(KotlinClass.BIG_INTEGER_ONE);
    }
}

Nomeação idiomática

O Kotlin tem convenções de chamada diferentes do Java, e elas podem mudar a forma como você nomeia as funções. Use @JvmName para criar nomes de modo que eles pareçam idiomáticos para as convenções de ambas as linguagens ou para que correspondam aos nomes das respectivas bibliotecas padrão.

Isso ocorre com mais frequência para funções e propriedades de extensão porque o local do tipo de receptor é diferente.

sealed class Optional
data class Some(val value: T): Optional()
object None : Optional()

@JvmName("ofNullable")
fun  T?.asOptional() = if (this == null) None else Some(this)
// FROM KOTLIN:
fun main(vararg args: String) {
    val nullableString: String? = "foo"
    val optionalString = nullableString.asOptional()
}
// FROM JAVA:
public static void main(String... args) {
    String nullableString = "Foo";
    Optional optionalString =
          Optionals.ofNullable(nullableString);
}

Sobrecargas de função para padrões

As funções com parâmetros que têm um valor padrão precisam usar @JvmOverloads. Sem essa anotação, é impossível invocar a função usando qualquer valor padrão.

Ao usar @JvmOverloads, inspecione os métodos gerados para garantir que eles façam sentido. Se isso não acontecer, execute uma ou as duas refatorações a seguir até conseguir um resultado satisfatório:

  • Mude a ordem dos parâmetros e dê preferências àqueles com padrões voltados para o final.
  • Mova os padrões para sobrecargas de funções manuais.

Incorreto: sem@JvmOverloads

class Greeting {
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Mr.", "Bob");
    }
}

Correto: anotação @JvmOverloads

class Greeting {
    @JvmOverloads
    fun sayHello(prefix: String = "Mr.", name: String) {
        println("Hello, $prefix $name")
    }
}
public class JavaClass {
    public static void main(String... args) {
        Greeting greeting = new Greeting();
        greeting.sayHello("Bob");
    }
}

Verificações de lint

Requisitos

  • Versão do Android Studio: 3.2 Canary 10 ou posterior
  • Versão do Plug-in do Android para Gradle: 3.2 ou posterior

Verificações compatíveis

Agora existem verificações do Android Lint para que você possa detectar e sinalizar alguns dos problemas de interoperabilidade descritos acima. Apenas problemas no Java (para consumo de Kotlin) são detectados no momento. Especificamente, as verificações compatíveis são:

  • nulidade desconhecida;
  • acesso de propriedade;
  • sem palavra-chave específica do Kotlin;
  • parâmetros lambda por último.

Android Studio

Para ativar essas verificações, acesse File > Preferences > Editor > Inspections e verifique as regras que você quer ativar em "Kotlin Interoperability":

Figura 1. Configurações de interoperabilidade do Kotlin no Android Studio.

Depois de verificar as regras que você quer ativar, as novas verificações serão executadas quando você executar as inspeções do código ( Analyze > Inspect Code…)

Compilações de linha de comando

Para ativar essas verificações em builds de linha de comando, adicione a linha a seguir no arquivo build.gradle:

Groovy

android {

    ...

    lintOptions {
        enable 'Interoperability'
    }
}

Kotlin

android {
    ...

    lintOptions {
        enable("Interoperability")
    }
}

Para ver o conjunto completo de configurações com suporte a lintOptions, consulte a referência Gradle DSL (link em inglês) para Android.

Em seguida, execute ./gradlew lint na linha de comando.