Guia de interoperabilidade entre Kotlin e Java

Este documento é um conjunto de regras para a criação de APIs públicas em Java e Kotlin com a intenção de que o código pareça idiomático quando consumido por outro idioma de destino.

Última atualização: 29/07/2024

Java (para consumo de Kotlin)

Nenhuma palavra-chave específica

Não use nenhuma das palavras-chave específicas do Kotlin como nome dos métodos. ou campos. Eles exigem o uso de acentos graves para escapar ao chamar de Kotlin Palavras-chave não relacionadas, palavras-chave modificadoras e identificadores especiais são permitidos.

Por exemplo, a função when do Mockito requer crases 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 ou os nomes das propriedades de extensão em Any para campos, a menos que seja absolutamente necessário. Embora os métodos e campos dos membros sempre têm precedência sobre as funções ou propriedades de extensão de Any, podem ser difícil ao ler o código para saber qual está sendo chamado.

Anotações de nulidade

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

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 a conversão de SAM precisam ser os últimos (link em inglês).

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

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

Como o FlowableOnSubscription está qualificado para a conversão de SAM, as chamadas de função de o método do Kotlin vai ficar assim:

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

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

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

Prefixos de propriedade

Para que um método seja representado como uma propriedade no Kotlin, o estilo "bean" é estrito de codificador-decodificador precisa ser usado.

Os métodos do acessador exigem um prefixo get ou, para métodos que retornam booleanos, um is .

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 de mutação 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 expostos como propriedades, não use prefixos não padrão como Acessores has, set ou sem o prefixo get. Métodos com prefixos não padrão ainda podem ser chamadas como funções, o que pode ser aceitável dependendo da do método.

Sobrecarga do operador

Atente-se aos nomes de métodos que permitem sintaxe especial do local da chamada (como sobrecarga de operadores no Kotlin). Certifique-se de que os nomes dos métodos então faz sentido usar 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 tiver funções ou propriedades de nível superior, sempre anote-o. com @file:JvmName("Foo") para fornecer um bom nome.

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

Adicione @file:JvmMultifileClass para combinar os membros de nível superior diversos 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 em uma linguagem idiomática de um jeito fácil. O Kotlin tem várias opções para definir essas interfaces, cada uma com uma pequena diferença.

Definição preferencial

Funções de ordem superior que precisam ser usadas em Java não use tipos de função que retornam Unit porque isso exigem que os autores das chamadas Java retornem Unit.INSTANCE. Em vez de inserir a função em linha digite a assinatura, use interfaces funcionais (SAM). Além disso, considere usar interfaces funcionais (SAM) em vez de comuns. ao definir aquelas que precisam ser usadas como lambdas, 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 invocada usando Kotlin:

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

Quando invocada usando 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. Isso permite que os autores da chamada a implementem com uma classe nomeada, e não apenas com lambdas (tanto em Kotlin como em 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;
});

Evitar 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. Comparable é um exemplo notório, já que ele foi feito para 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 object : .... , o que permite que ele tenha um estado, fornecendo uma dica ao 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 com parâmetro genérico Nothing é exposto como tipos brutos para Java. Bruto são raramente usados em Java e devem 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 somente leitura compartilhadas ou sem proprietário de APIs públicas, junte em um contêiner não modificável ou fazer uma cópia defensiva. Apesar do Kotlin aplicando sua propriedade somente leitura, essa restrição não será aplicada lado. Sem o wrapper ou a cópia defensiva, as invariantes podem ser violadas por retornando uma referência de coleção de longa duração.

Funções complementares

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

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

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 só ficam disponíveis como nomes estranhos instância "getters" no campo Companion estático. Usando @JvmStatic de @JvmField move os "getters" com um nome estranho a 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 que pareçam idiomáticos para as convenções de ambas as linguagens ou para corresponder às respectivas bibliotecas padrão e nomeação.

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

sealed class Optional<T : Any>
data class Some<T : Any>(val value: T): Optional<T>()
object None : Optional<Nothing>()

@JvmName("ofNullable")
fun <T> 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<String> 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 cada um deles que façam sentido. Caso contrário, execute uma das refatorações a seguir ou ambas até ficar satisfeito:

  • Mude a ordem dos parâmetros para dar preferência aos que têm o padrão para o fim.
  • 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 que ajudarão você a detectar e sinalizar alguns os problemas de interoperabilidade descritos anteriormente. Somente problemas em Java (para Kotlin consumo de energia) são detectados. 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, vá para Arquivo > Preferências > Editor > inspeções e Marque as regras que você quer ativar em "Interoperabilidade do Kotlin":

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

Depois de marcar as regras que você quer ativar, as novas verificações executar ao executar as inspeções de código (Analyze > Inspect Code...)

Builds de linha de comando

Para ativar essas verificações nos builds de linha de comando, adicione a seguinte linha em seu 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.