Kotlin sob o capô - veja bytecode descompilado



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

imagem

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; } } // $FF: synthetic method public static int factorial$default(int var0, int var1, int var2, Object var3) { if ((var2 & 2) != 0) { var1 = 1; } return factorial(var0, var1); } } 

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 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.

imagem

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); } // $FF: synthetic method @NotNull public static Test copy$default(Test var0, String var1, String var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = var0.argumentValue; } if ((var3 & 2) != 0) { var2 = var0.argumentValue2; } return var0.copy(var1, var2); } @NotNull public String toString() { return "Test(argumentValue=" + this.argumentValue + ", argumentValue2=" + this.argumentValue2 + ")"; } public int hashCode() { return (this.argumentValue != null ? this.argumentValue.hashCode() : 0) * 31 + (this.argumentValue2 != null ? this.argumentValue2.hashCode() : 0); } public boolean equals(@Nullable Object var1) { if (this != var1) { if (var1 instanceof Test) { Test var2 = (Test)var1; if (Intrinsics.areEqual(this.argumentValue, var2.argumentValue) && Intrinsics.areEqual(this.argumentValue2, var2.argumentValue2)) { return true; } } return false; } else { return true; } } } 

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; } // $FF: synthetic method public Something(int var1, int var2, DefaultConstructorMarker var3) { if ((var2 & 1) != 0) { var1 = 0; } this(var1); } public Something() { this(0, 1, (DefaultConstructorMarker)null); } } public final class Test { public final void test() { Something something = new Something(0, 1, (DefaultConstructorMarker)null); something = something.inc(); } } 

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; // $FF: synthetic method private User(@NotNull String name) { Intrinsics.checkParameterIsNotNull(name, "name"); super(); this.name = name; } @NotNull public static final String upperCase_impl/* $FF was: upperCase-impl*/(String $this) { if ($this == null) { throw new TypeCastException("null cannot be cast to non-null type java.lang.String"); } else { String var10000 = $this.toUpperCase(); Intrinsics.checkExpressionValueIsNotNull(var10000, "(this as java.lang.String).toUpperCase()"); return var10000; } } @NotNull public static String constructor_impl/* $FF was: constructor-impl*/(@NotNull String name) { Intrinsics.checkParameterIsNotNull(name, "name"); return name; } // $FF: synthetic method @NotNull public static final User box_impl/* $FF was: box-impl*/(@NotNull String v) { Intrinsics.checkParameterIsNotNull(v, "v"); return new User(v); } @NotNull public static String toString_impl/* $FF was: toString-impl*/(String var0) { return "User(name=" + var0 + ")"; } public static int hashCode_impl/* $FF was: hashCode-impl*/(String var0) { return var0 != null ? var0.hashCode() : 0; } public static boolean equals_impl/* $FF was: equals-impl*/(String var0, @Nullable Object var1) { if (var1 instanceof User) { String var2 = ((User)var1).unbox-impl(); if (Intrinsics.areEqual(var0, var2)) { return true; } } return false; } public static final boolean equals_impl0/* $FF was: equals-impl0*/(@NotNull String p1, @NotNull String p2) { Intrinsics.checkParameterIsNotNull(p1, "p1"); Intrinsics.checkParameterIsNotNull(p2, "p2"); throw null; } // $FF: synthetic method @NotNull public final String unbox_impl/* $FF was: unbox-impl*/() { return this.name; } public String toString() { return toString-impl(this.name); } public int hashCode() { return hashCode-impl(this.name); } public boolean equals(Object var1) { return equals-impl(this.name, var1); } } 

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.

Source: https://habr.com/ru/post/pt425077/


All Articles