
Visualizar o bytecode do Kotlin descompilado em Java talvez seja a melhor maneira de entender como ele ainda funciona e como algumas construções de linguagem afetam o desempenho. Muitos já fizeram isso sozinhos há muito tempo, portanto, este artigo será especialmente relevante para iniciantes e para aqueles que dominam o Java há muito tempo e decidiram usar o Kotlin recentemente.
Eu sinto falta especificamente de momentos bem comuns e conhecidos, já que, provavelmente, não faz sentido pela centésima vez escrever sobre a geração de getters / setters para var e coisas semelhantes. Então, vamos começar.
Como visualizar bytecode descompilado no Intellij Idea?
Muito simples - basta abrir o arquivo desejado e selecionar no menu Ferramentas -> Kotlin -> Mostrar Bytecode do Kotlin

Em seguida, na janela exibida, basta clicar em Descompilar

Para visualizar a versão do Kotlin 1.3-RC será usada.
Agora, finalmente, vamos para a parte principal.
objeto
Kotlin
object Test
Java descompilado
public final class Test { public static final Test INSTANCE; static { Test var0 = new Test(); INSTANCE = var0; } }
Suponho que todo mundo que lida com Kotlin sabe que o objeto cria um singleton. No entanto, está longe de ser óbvio para todos exatamente qual singleton é criado e se é seguro para threads.
O código descompilado mostra que o singleton recebido é semelhante à implementação ansiosa do singleton; ele é criado no momento em que o carregador de classe carrega a classe. Por um lado, um bloco estático é executado quando é carregado por um driver de classe, que por si só é seguro para threads. Por outro lado, se houver mais de um motorista de sala de aula, você não poderá sair com uma cópia.
extensões
Kotlin
fun String.getEmpty(): String { return "" }
Java descompilado
public final class TestKt { @NotNull public static final String getEmpty(@NotNull String $receiver) { Intrinsics.checkParameterIsNotNull($receiver, "receiver$0"); return ""; } }
Aqui, em geral, tudo está claro - as extensões são apenas açúcar sintático e compiladas em um método estático regular.
Se alguém ficou confuso com a linha com Intrinsics.checkParameterIsNotNull, tudo fica transparente lá - em todas as funções com argumentos não nulos, o Kotlin adiciona uma verificação nula e lança uma exceção se você escorregou em um
porco nulo, embora prometesse não fazê-lo nos argumentos. É assim:
public static void checkParameterIsNotNull(Object value, String paramName) { if (value == null) { throwParameterIsNullException(paramName); } }
O que é típico se você escrever não uma função, mas uma propriedade de extensão
val String.empty: String get() { return "" }
Como resultado, obtemos exatamente a mesma coisa que obtivemos no método String.getEmpty ()
inline
Kotlin
inline fun something() { println("hello") } class Test { fun test() { something() } }
Java descompilado
public final class Test { public final void test() { String var1 = "hello"; System.out.println(var1); } } public final class TestKt { public static final void something() { String var1 = "hello"; System.out.println(var1); } }
Com o inline, tudo é bastante simples - uma função marcada como inline é simples e completamente inserida no local de onde foi chamada. Curiosamente, ele também se compila em estática, provavelmente para interoperabilidade com Java.
Todo o poder do inline é revelado no momento em que
lambda aparece nos argumentos:
Kotlin
inline fun something(action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } }
Java descompilado
public final class Test { public final void test() { String var1 = "hello"; System.out.println(var1); var1 = "world"; System.out.println(var1); } } public final class TestKt { public static final void something(@NotNull Function0 action) { Intrinsics.checkParameterIsNotNull(action, "action"); action.invoke(); String var2 = "world"; System.out.println(var2); } }
Na parte inferior, a estática é novamente visível e na parte superior fica claro que o lambda no argumento da função também está alinhado e não cria uma classe anônima adicional, como é o caso do lambda usual no Kotlin.
Por esse motivo, muitos dos conhecimentos inline de Kotlin terminam, mas há mais 2 pontos interessantes, como noinline e crossinline. Essas são palavras-chave que podem ser atribuídas a um lambda, que é um argumento em uma função embutida.
Kotlin
inline fun something(noinline action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } }
Java descompilado
public final class Test { public final void test() { Function0 action$iv = (Function0)null.INSTANCE; action$iv.invoke(); String var2 = "world"; System.out.println(var2); } } public final class TestKt { public static final void something(@NotNull Function0 action) { Intrinsics.checkParameterIsNotNull(action, "action"); action.invoke(); String var2 = "world"; System.out.println(var2); } }
Com esse registro, o IDE começa a indicar que esse tipo de linha é inútil um pouco menos que completamente. E compila exatamente o mesmo que Java - cria Function0. Por que descompilar com estranho (Function0) null.INSTANCE; - Não faço ideia, provavelmente isso é um bug do descompilador.
O crossinline, por sua vez, faz exatamente o mesmo que um inline regular (ou seja, se nada for escrito antes do lambda no argumento), com algumas exceções, o retorno não poderá ser gravado no lambda, o que é necessário para bloquear a capacidade de terminar subitamente a função que chama inline. No sentido, você pode escrever algo, mas, primeiro, o IDE jurará, e segundo, ao compilar, obtemos
'return' não é permitido aqui
No entanto, o bytecode entre linhas não difere da linha padrão - a palavra-chave é usada apenas pelo compilador.
infix
Kotlin
infix fun Int.plus(value: Int): Int { return this+value } class Test { fun test() { val result = 5 plus 3 } }
Java descompilado
public final class Test { public final void test() { int result = TestKt.plus(5, 3); } } public final class TestKt { public static final int plus(int $receiver, int value) { return $receiver + value; } }
Funções Infix são compiladas como extensões para estáticas regulares
tailrec
Kotlin
tailrec fun factorial(step:Int, value: Int = 1):Int { val newValue = step*value return if (step == 1) newValue else factorial(step - 1,newValue) }
Java descompilado
public final class TestKt { public static final int factorial(int step, int value) { while(true) { int newValue = step * value; if (step == 1) { return newValue; } int var10000 = step - 1; value = newValue; step = var10000; } }
tailrec é uma coisa bastante divertida. Como você pode ver no código, a recursão simplesmente entra em um ciclo muito menos legível, mas o desenvolvedor pode dormir em paz, pois nada sai do Stackoverflow no momento mais desagradável. Outra coisa na vida real é raramente encontrar tailrec.
reificado
Kotlin
inline fun <reified T>something(value: Class<T>) { println(value.simpleName) }
Java descompilado
public final class TestKt { private static final void something(Class value) { String var2 = value.getSimpleName(); System.out.println(var2); } }
Em geral, sobre o conceito de se reificado e por que é necessário, você pode escrever um artigo inteiro. Em resumo, o acesso ao próprio tipo em Java não é possível em tempo de compilação, porque Antes de compilar Java, ninguém sabe o que estará lá. Kotlin é outra questão. A palavra-chave reificada pode ser usada apenas em funções embutidas, que, como já observado, são simplesmente copiadas e coladas nos lugares certos; portanto, já durante a "chamada" da função, o compilador
já está ciente de que tipo é e pode modificar o bytecode.
Você deve prestar atenção ao fato de que uma função estática com um nível de acesso
privado é compilada no bytecode, o que significa que isso não funcionará no Java. A propósito, devido à reificação no anúncio da Kotlin
“100% interoperável com Java e Android” , é obtida pelo menos imprecisão.

Talvez afinal 99%?
init
Kotlin
class Test { constructor() constructor(value: String) init { println("hello") } }
Java descompilado
public final class Test { public Test() { String var1 = "hello"; System.out.println(var1); } public Test(@NotNull String value) { Intrinsics.checkParameterIsNotNull(value, "value"); super(); String var2 = "hello"; System.out.println(var2); } }
Em geral, com o init, tudo é simples - esta é uma função embutida normal, que funciona
antes de chamar o código do próprio construtor.
classe de dados
Kotlin
data class Test(val argumentValue: String, val argumentValue2: String) { var innerValue: Int = 0 }
Java descompilado
public final class Test { private int innerValue; @NotNull private final String argumentValue; @NotNull private final String argumentValue2; public final int getInnerValue() { return this.innerValue; } public final void setInnerValue(int var1) { this.innerValue = var1; } @NotNull public final String getArgumentValue() { return this.argumentValue; } @NotNull public final String getArgumentValue2() { return this.argumentValue2; } public Test(@NotNull String argumentValue, @NotNull String argumentValue2) { Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue"); Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2"); super(); this.argumentValue = argumentValue; this.argumentValue2 = argumentValue2; } @NotNull public final String component1() { return this.argumentValue; } @NotNull public final String component2() { return this.argumentValue2; } @NotNull public final Test copy(@NotNull String argumentValue, @NotNull String argumentValue2) { Intrinsics.checkParameterIsNotNull(argumentValue, "argumentValue"); Intrinsics.checkParameterIsNotNull(argumentValue2, "argumentValue2"); return new Test(argumentValue, argumentValue2); }
Honestamente, eu não queria mencionar as classes de datas, sobre as quais muito já foi dito, mas, no entanto, há alguns pontos dignos de atenção. Antes de tudo, vale a pena notar que apenas as variáveis que foram passadas para o construtor entram em igual a / hashCode / copy / toString. Para a pergunta por que isso acontece, Andrei Breslav respondeu que pegar campos que não foram transferidos no construtor também é difícil e íngreme. A propósito, é impossível herdar da data da classe, a verdade é apenas porque
durante a herança o código gerado não estaria correto . Em segundo lugar, vale a pena observar o método component1 () para obter o valor do campo. Tantos métodos componentN () são gerados quanto há argumentos no construtor. Parece inútil, mas você realmente precisa dela para uma
declaração de desestruturação .
declaração de desestruturação
Por exemplo, usaremos a classe de data do exemplo anterior e adicionaremos o seguinte código:
Kotlin
class DestructuringDeclaration { fun test() { val (one, two) = Test("hello", "world") } }
Java descompilado
public final class DestructuringDeclaration { public final void test() { Test var3 = new Test("hello", "world"); String var1 = var3.component1(); String two = var3.component2(); } }
Geralmente, esse recurso está acumulando poeira em uma prateleira, mas às vezes pode ser útil, por exemplo, ao trabalhar com o conteúdo do mapa.
operador
Kotlin
class Something(var likes: Int = 0) { operator fun inc() = Something(likes+1) } class Test() { fun test() { var something = Something() something++ } }
Java descompilado
public final class Something { private int likes; @NotNull public final Something inc() { return new Something(this.likes + 1); } public final int getLikes() { return this.likes; } public final void setLikes(int var1) { this.likes = var1; } public Something(int likes) { this.likes = likes; }
A palavra-chave operator é necessária para substituir algum operador de idioma para uma classe específica. Honestamente, nunca vi alguém usar isso, mas, no entanto, existe uma oportunidade, mas não há mágica por dentro. De fato, o compilador simplesmente substitui o operador pela função desejada, assim como as tipealias são substituídas por um tipo específico.
E sim, se agora você pensou no que aconteceria se redefinir o operador de identidade (=== qual), então eu me apressei em incomodá-lo, este é um operador que não pode ser redefinido.
classe inline
Kotlin
inline class User(internal val name: String) { fun upperCase(): String { return name.toUpperCase() } } class Test { fun test() { val user = User("Some1") println(user.upperCase()) } }
Java descompilado
public final class Test { public final void test() { String user = User.constructor-impl("Some1"); String var2 = User.upperCase-impl(user); System.out.println(var2); } } public final class User { @NotNull private final String name;
Das limitações - você pode usar apenas um argumento no construtor, no entanto, é compreensível, uma vez que a classe inline é geralmente um invólucro sobre qualquer variável. Uma classe embutida pode conter métodos, mas eles são apenas estáticos. Também é óbvio que todos os métodos necessários foram adicionados para dar suporte à interoperação Java.
Sumário
Não esqueça que, em primeiro lugar, o código nem sempre será descompilado corretamente e, em segundo lugar, nem todo código pode ser descompilado. No entanto, a capacidade de assistir o código descompilado do Kotlin por si só é muito interessante e pode esclarecer muito.