Jetbrains

Demonstração dos novos recursos de linguagem no Kotlin 1.4.30

Estamos planejando adicionar novos recursos de linguagem ao Kotlin 1.5, e você já pode experimentá-los no Kotlin 1.4.30:

Para experimentar esses novos recursos, você precisa especificar a versão de linguagem 1.5.

A nova cadência de lançamentos significa que o Kotlin 1.5 será lançado em alguns meses, mas novos recursos já estão disponíveis para demonstração na versão 1.4.30. Como seu feedback inicial é essencial para nós, não deixe de experimentar esses novos recursos agora mesmo!

Estabilização de classes inline value

Classes inline estão disponíveis em Alpha desde o Kotlin 1.3 e, na versão 1.4.30, elas foram promovidas para Beta.

O Kotlin 1.5 estabiliza o conceito de classes inline, mas o torna parte de um recurso mais geral, classes value, que descreveremos mais adiante neste post.

Começaremos com uma recapitulação sobre como funcionam as classes inline. Se você já está familiarizado com classes inline, pode pular esta seção e ir diretamente para as novas alterações.

Como lembrete rápido, uma classe inline elimina um wrapper em torno de um valor:

inline class Color(val rgb: Int)

Uma classe inline pode ser um wrapper para um tipo primitivo e para qualquer tipo de referência, como String.

O compiler substitui instâncias da classe inline (em nosso exemplo, a instância Color) pelo tipo subjacente (Int) no bytecode, quando possível:

fun changeBackground(color: Color) 
val blue = Color(255)
changeBackground(blue)

Nos bastidores, o compiler gera a função changeBackground com um nome deformado usando Int como um parâmetro e transmite a constante 255 diretamente sem criar um wrapper no local de chamada:

fun changeBackground-euwHqFQ(color: Int) 
changeBackground-euwHqFQ(255) // no extra object is allocated! 

O nome é deformado para permitir a sobrecarga contínua de funções que usam instâncias de diferentes classes inline e para evitar invocações acidentais do código Java que podem violar as restrições internas de uma classe inline. Leia abaixo para descobrir como torná-lo utilizável em Java.

O wrapper nem sempre é eliminado no bytecode. Isso acontece apenas quando possível e funciona de forma muito semelhante a tipos primitivos integrados. Quando você define uma variável do tipo Color ou a transmite diretamente para uma função, ela é substituída pelo valor subjacente:

val color = Color(0)        // primitive
changeBackground(color)     // primitive

Neste exemplo, a variável color tem o tipo Color durante a compilação, mas é substituída por Int no bytecode.

Porém, se você a armazenar em uma coleção ou transmiti-la para uma função genérica, ela será empacotada em um objeto regular do tipo Color:

genericFunc(color)         // boxed
val list = listOf(color)   // boxed
val first = list.first()   // unboxed back to primitive

O empacotamento e o desempacotamento são feitos automaticamente pelo compiler. Você não precisa fazer nada a respeito, mas é útil entender os detalhes internos.

Alterando o nome JVM para chamadas Java

A partir da versão 1.4.30, você pode alterar o nome JVM de uma função usando uma classe inline como parâmetro para torná-la utilizável em Java. Por padrão, esses nomes são deformados para evitar usos acidentais do Java ou sobrecargas conflitantes (como changeBackground-euwHqFQ no exemplo acima).

Se você anotar uma função com @JvmName, isso alterará o nome dessa função no bytecode e permitirá sua chamada no Java e a transmissão direta de um valor:

// Kotlin declarations
inline class Timeout(val millis: Long)

val Int.millis get() = Timeout(this.toLong())
val Int.seconds get() = Timeout(this * 1000L)

@JvmName("greetAfterTimeoutMillis")
fun greetAfterTimeout(timeout: Timeout)

// Kotlin usage
greetAfterTimeout(2.seconds)

// Java usage
greetAfterTimeoutMillis(2000);

Como sempre com uma função anotada com @JvmName, no Kotlin, você a chama pelo seu nome Kotlin. O uso do Kotlin é seguro para tipos, já que você só pode transmitir um valor do tipo Timeout como um argumento, e as unidades são óbvias pelo uso.

No Java, você pode transmitir um valor long diretamente. Ele não é mais seguro para tipos e é por isso que não funciona por padrão. Se você vir greetAfterTimeout(2) no código, não fica imediatamente óbvio se o 2 refere-se a segundos, milissegundos ou anos.

Ao fornecer a anotação, você enfatiza explicitamente que pretende que essa função seja chamada do Java. Um nome descritivo ajuda a evitar confusão: adicionar o sufixo “Millis” ao nome JVM torna as unidades claras para usuários Java.

Blocos Init

Outra melhoria para classes inline na versão 1.4.30 é que agora você pode definir a lógica de inicialização no bloco init:

inline class Name(val s: String) {
   init {
       require(s.isNotEmpty())
   }
}

Antes, isso era proibido.

Você pode ler mais detalhes sobre classes inline no KEEP correspondente, na documentação e na discussão deste issue.

Classes inline value

O Kotlin 1.5 estabiliza o conceito de classes inline, mas o torna parte de um recurso mais geral, classes value.

Até agora, as classes “inline” constituíam um recurso de linguagem separado, mas agora estão se tornando uma otimização JVM específica para uma classe value com um só parâmetro. Classes value representam um conceito mais geral e suportarão diferentes otimizações: classes inline agora e classes primitivas Valhalla no futuro quando o projeto Valhalla estiver disponível (mais sobre isso abaixo).

A única coisa que muda para você no momento é a sintaxe. Como uma classe inline é uma classe de valor otimizado, você precisa declará-la de forma diferente do que antes:

@JvmInline
value class Color(val rgb: Int)

Você define uma classe value com um parâmetro do construtor e a anota com @JvmInline. Esperamos que todos usem essa nova sintaxe a partir do Kotlin 1.5. A classe inline com sintaxe antiga continuará a funcionar por um certo tempo. Ela será descontinuada com um aviso na versão 1.5, que incluirá uma opção para migrar todas as suas declarações automaticamente. Posteriormente, ela será descontinuada com um erro.

Classes value

Uma classe value representa uma entidade imutável com dados. No momento, uma classe value pode conter apenas uma propriedade para suportar o caso de uso de classes inline “antigas”.

Nas versões futuras do Kotlin com suporte total para esse recurso, será possível definir classes value com muitas propriedades. Todos os valores devem ser vals somente leitura:

value class Point(val x: Int, val y: Int)

Classes value não têm identidade: elas são completamente definidas pelos dados armazenados e verificações de identidade === não são permitidos para elas. A verificação de igualdade == compara automaticamente os dados subjacentes.

Essa qualidade “sem identidade” das classes value permite otimizações futuras significativas: a chegada do projeto Valhalla à JVM permitirá que as classes value sejam implementadas como classes primitivas JVM nos bastidores.

A restrição de imutabilidade e, portanto, a possibilidade de otimizações para o Valhalla, torna as classes value diferentes das classes data.

Otimização futura para Valhalla

O Projeto Valhalla introduz um novo conceito para Java e JVM: classes primitivas.

O principal objetivo das classes primitivas é combinar primitivas de grande desempenho com os benefícios orientados a objetos das classes JVM regulares. Classes primitivas são portadores de dados cujas instâncias podem ser armazenadas em variáveis, na pilha de computação, e operadas diretamente, sem cabeçalhos e ponteiros. Nesse sentido, elas são semelhantes a valores primitivos, como int, long, etc. (no Kotlin, você não trabalha com tipos primitivos diretamente, mas o compilador os gera nos bastidores).

Uma vantagem importante das classes primitivas é que elas permitem o layout simples e denso de objetos na memória. No momento, Array<Point> é um conjunto de referências. Com suporte ao Valhalla, ao definir Point como uma classe primitiva (na terminologia Java) ou como uma classe value com otimização subjacente (na terminologia Kotlin), a JVM pode otimizá-la e armazenar uma matriz de Points em um layout “simples”, como uma matriz de muitos xs e ys diretamente, e não como uma matriz de referências.

Estamos realmente ansiosos para as próximas mudanças na JVM e queremos que o Kotlin se beneficie com todas elas. Ao mesmo tempo, não queremos forçar nossa comunidade a depender das novas versões de JVM para usar classes value e, portanto, ofereceremos suporte para as versões JVM anteriores também. Ao compilar o código para a JVM com suporte para Valhalla, as otimizações mais recentes da JVM funcionarão para as classes value.

Métodos mutantes

Há muito mais a dizer sobre a funcionalidade das classes value. Como as classes value representam dados “imutáveis”, métodos mutantes, como aqueles em Swift, são possíveis para elas. Um método mutante é quando uma função membro ou setter de propriedade retorna uma nova instância em vez de atualizar uma existente, e o principal benefício é que você os usa com uma sintaxe familiar. Isso ainda precisa ser prototipado na linguagem.

Mais detalhes

A anotação @JvmInline é específica para JVM. Em outros back-ends, as classes value podem ser implementadas de maneira diferente. Por exemplo, como estruturas Swift em Kotlin/Native.

Você pode ler os detalhes sobre classes value na Nota de design para classes value Kotlin ou pode assistir a um trecho da conversa “Um olhar para o futuro” de Roman Elizarov.

Suporte para registros JVM

Outra melhoria futura no ecossistema JVM são os registros Java. Eles são análogos às classes data do Kotlin e, em essência, são portadores de dados.

Os registros Java não seguem a convenção JavaBeans e têm os métodos x() and y() em vez dos familiares getX() and getY().

A interoperabilidade com Java sempre foi e continua sendo uma prioridade para o Kotlin. Portanto, o código Kotlin “entende” os novos registros Java e os vê como classes com propriedades Kotlin. Isso funciona como para classes Java regulares seguindo a convenção JavaBeans:

// Java
record Point(int x, int y) { }
// Kotlin
fun foo(point: Point) {
    point.x // seen as property
    point.x() // also works
}

Principalmente por razões de interoperabilidade, você pode anotar sua classe data com @JvmRecord para ter novos métodos de registros JVM gerados:

@JvmRecord
data class Point(val x: Int, val y: Int)

A anotação @JvmRecord faz com que o compilador gere métodos x() e y() em vez dos métodos getX() e getY() padrão. Presumimos que você só precisa usar essa anotação para preservar a API da classe ao convertê-la de Java em Kotlin. Em todos os outros casos de uso, as classes data familiares do Kotlin podem ser usadas sem problemas.

Essa anotação estará disponível apenas se você compilar o código Kotlin para a versão 15+ da versão JVM. Você pode ler mais sobre este recurso no KEEP correspondente ou na documentação, bem como na discussão deste issue.

Melhorias em interfaces e classes sealed

Quando você cria uma classe sealed, ela restringe a hierarquia a subclasses definidas, o que permite verificações exaustivas em branches when. No Kotlin 1.4, a hierarquia de classes selada vem com duas restrições. Em primeiro lugar, a classe superior não pode ser uma interface sealed, ela deve ser uma classe. Em segundo lugar, todas as subclasses devem estar localizadas no mesmo arquivo.

O Kotlin 1.5 remove ambas as restrições: agora você pode tornar uma interface sealed. As subclasses (tanto para classes seladas quanto para interfaces sealed) devem estar localizadas na mesma unidade de compilação e no mesmo pacote da superclasse, mas agora podem estar localizadas em arquivos diferentes.

sealed interface Expr
data class Const(val number: Double) : Expr
data class Sum(val e1: Expr, val e2: Expr) : Expr
object NotANumber : Expr

fun eval(expr: Expr): Double = when(expr) {
    is Const -> expr.number
    is Sum -> eval(expr.e1) + eval(expr.e2)
    NotANumber -> Double.NaN
}

Classes sealed, e agora interfaces, são úteis para definir hierarquias de tipo abstrato de dados (ADT).

Outro caso de uso importante que agora pode ser bem endereçado com interfaces sealed é fechar uma interface para herança e implementação fora da biblioteca. Definir uma interface como sealed restringe sua implementação à mesma unidade de compilação e ao mesmo pacote, o que, no caso de uma biblioteca, torna impossível a implementação fora da biblioteca.

Por exemplo, a interface Job do pacote kotlinx.coroutines destina-se apenas a ser implementada dentro da biblioteca kotlinx.coroutines. Torná-la sealed torna essa intenção explícita:

package kotlinx.coroutines
sealed interface Job { ... }

Como usuário da biblioteca, você não tem mais permissão para definir sua própria subclasse de Job. Isso sempre foi “implícito”, mas com interfaces sealed, o compiler pode proibir isso formalmente.

Usando o suporte da JVM no futuro

O suporte de demonstração para classes sealed foi introduzido no Java 15 e na JVM. No futuro, usaremos o suporte da JVM natural para classes sealed se você compilar o código Kotlin para a JVM mais recente (provavelmente JVM 17 ou posterior, quando esse recurso se tornar estável).

Em Java, você lista explicitamente todas as subclasses da classe ou interface sealed fornecida:

// Java
public sealed interface Expression
    permits Const, Sum, NotANumber { ... }

Essas informações são armazenadas no arquivo de classe usando o novo atributo PermittedSubclasses. A JVM reconhece classes sealed em tempo de execução e evita sua extensão por subclasses não autorizadas.

No futuro, quando você compilar o Kotlin para a JVM mais recente, esse novo suporte da JVM para classes sealed será usado. Nos bastidores, o compiler gerará uma lista de subclasses permitidas no bytecode para garantir que haja suporte da JVM e verificações adicionais de tempo de execução.

// for JVM 17 or later
Expr::class.java.permittedSubclasses // [Const, Sum, NotANumber]

No Kotlin, você não precisa especificar a lista de subclasses. O compiler gerará essa lista com base nas subclasses declaradas no mesmo pacote.

A capacidade de especificar explicitamente as subclasses de uma superclasse ou interface pode ser adicionada posteriormente como uma especificação opcional. No momento, suspeitamos que isso não será necessário, mas gostaríamos de saber mais sobre seus casos de uso e se você precisa dessa funcionalidade!

Observe que, para versões JVM mais antigas, é teoricamente possível definir uma subclasse Java para a interface sealed Kotlin, mas não faça isso. Como o suporte da JVM para subclasses permitidas ainda não está disponível, essa restrição é aplicada apenas pelo compilador Kotlin. Adicionaremos avisos do IDE para evitar que isso seja feito acidentalmente. No futuro, o novo mecanismo será usado para as versões mais recentes da JVM para garantir que não haja subclasses “não autorizadas” do Java.

Você pode ler mais sobre interfaces sealed e as restrições de classes sealed afrouxadas no KEEP correspondente ou na documentação e veja a discussão neste issue.

Como experimentar os novos recursos

Você precisa usar o Kotlin 1.4.30. Especifique a versão 1.5 da linguagem para habilitar os novos recursos:

compileKotlin {
    kotlinOptions {
        languageVersion = "1.5"
        apiVersion = "1.5"
    }
}

Para experimentar os registros JVM, você também precisa usar jvmTarget 15 e habilitar os recursos de demonstração da JVM: adicione as opções do compilador -language-version 1.5 e -Xjvm-enable-preview.

Notas de pré-lançamento

Observe que o suporte para os novos recursos é experimental, e o suporte à versão 1.5 da linguagem está no status de pré-lançamento. Definir a versão da linguagem para 1.5 no compiler Kotlin 1.4.30 é equivalente a usar a versão de demonstração 1.5 M0. As garantias de compatibilidade retroativa não abrangem versões de pré-lançamento. Os recursos e a API podem mudar nas versões subsequentes. Quando chegarmos a um Kotlin 1.5-RC final, todos os binários produzidos pelas versões de pré-lançamento serão proibidos pelo compilador, e você precisará recompilar tudo o que foi compilado pelo 1.5‑Mx.

Compartilhe suas opiniões

Experimente os novos recursos descritos neste post e compartilhe seu feedback. Você pode encontrar mais detalhes em KEEPs e participar das discussões no YouTrack, além de relatar novos issues se algo não funcionar para você. Compartilhe suas constatações sobre como os novos recursos atendem aos casos de uso nos seus projetos!

Leituras e discussões adicionais:

Powered by WPeMatico