
Ver el bytecode de Kotlin descompilado en Java es quizás la mejor manera de entender cómo funciona y cómo algunas construcciones de lenguaje afectan el rendimiento. Muchos lo han hecho solos hace mucho tiempo, por lo que este artículo será especialmente relevante para los principiantes y aquellos que han dominado Java durante mucho tiempo y decidieron usar Kotlin recientemente.
Voy a perder intencionalmente momentos muy tristes y conocidos, ya que, probablemente, no tiene sentido escribir por centésima vez sobre la generación de captadores / establecedores para var y cosas similares. Entonces comencemos.
¿Cómo ver el bytecode descompilado en Intellij Idea?
Bastante simple: solo abra el archivo deseado y seleccione en el menú Herramientas -> Kotlin -> Mostrar código de bytes de Kotlin

Luego, en la ventana que aparece, simplemente haga clic en Descompilar

Para ver, se utilizará la versión de Kotlin 1.3-RC.
Ahora, finalmente, pasemos a la parte principal.
objeto
Kotlin
object Test
Java descompilado
public final class Test { public static final Test INSTANCE; static { Test var0 = new Test(); INSTANCE = var0; } }
Supongo que todos los que tratan con Kotlin saben que ese objeto crea un singleton. Sin embargo, está lejos de ser obvio para todos exactamente qué singleton se crea y si es seguro para subprocesos.
El código descompilado muestra que el singleton recibido es similar a la implementación entusiasta del singleton, se crea en el momento en que el cargador de clases carga la clase. Por un lado, un bloque estático se ejecuta cuando lo carga un controlador de clase, que en sí mismo es seguro para subprocesos. Por otro lado, si hay más de un conductor de aula, entonces no puede salir con una copia.
extensiones
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 ""; } }
Aquí, en general, todo está claro: las extensiones son solo azúcar sintáctico y se compilan en un método estático regular.
Si alguien estaba confundido por la línea con Intrinsics.checkParameterIsNotNull, entonces todo es transparente allí: en todas las funciones con argumentos no anulables, Kotlin agrega un cheque nulo y lanza una excepción si resbaló un
cerdo nulo, aunque prometió no hacerlo en los argumentos. Se ve así:
public static void checkParameterIsNotNull(Object value, String paramName) { if (value == null) { throwParameterIsNullException(paramName); } }
Lo que es típico si escribe no una función, sino una propiedad de extensión
val String.empty: String get() { return "" }
Luego, como resultado, obtenemos exactamente lo mismo que obtuvimos para el método String.getEmpty ()
en línea
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); } }
Con inline, todo es bastante simple: una función marcada como inline simplemente se inserta completa y completamente en el lugar desde donde se llamó. Curiosamente, también se compila en estática, probablemente por interoperabilidad con Java.
Todo el poder de la línea se revela en el momento en que aparece
lambda en los 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); } }
En la parte inferior, la estática es nuevamente visible, y en la parte superior está claro que la lambda en el argumento de la función también está en línea, y no crea una clase anónima adicional, como es el caso de la lambda habitual en Kotlin.
Alrededor de esto, muchos de los conocimientos en línea de Kotlin terminan, pero hay 2 puntos más interesantes, a saber, noinline y crossinline. Estas son palabras clave que se pueden asignar a una lambda, que es un argumento en una función en línea.
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); } }
Con tal registro, el IDE comienza a indicar que tal línea es inútil un poco menos que completamente. Y compila exactamente lo mismo que Java: crea Function0. ¿Por qué descompilado con extraño (Function0) null.INSTANCE; - No tengo idea, lo más probable es que se trate de un error del descompilador.
crossinline, a su vez, hace exactamente lo mismo que una línea regular (es decir, si no se escribe nada antes del lambda en el argumento), con algunas excepciones, el retorno no se puede escribir en el lambda, que es necesario para bloquear la capacidad de terminar repentinamente la función que llama en línea. En el sentido, puede escribir algo, pero en primer lugar, el IDE jurará, y en segundo lugar, al compilar, obtenemos
'volver' no está permitido aquí
Sin embargo, el bytecode crossinline no difiere del predeterminado en línea: la palabra clave solo la usa el compilador.
infijo
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; } }
Las funciones de infijo se compilan como extensiones de estática regular
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 es algo bastante entretenido. Como puede ver en el código, la recursión simplemente entra en un ciclo mucho menos legible, pero el desarrollador puede dormir tranquilo, ya que nada saldrá de Stackoverflow en el momento más desagradable. Otra cosa en la vida real rara vez es 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); } }
En general, sobre el concepto de reified en sí y por qué es necesario, puede escribir un artículo completo. En resumen, el acceso al tipo en sí mismo en Java no es posible en tiempo de compilación, porque Antes de compilar Java, nadie sabe qué habrá allí. Kotlin es otro asunto. La palabra clave reified solo se puede usar en funciones en línea, que, como ya se señaló, simplemente se copian y pegan en los lugares correctos, por lo tanto, ya durante la "llamada" de la función, el compilador
ya sabe de qué tipo es y puede modificar el código de bytes.
Debe prestar atención al hecho de que una función estática con un nivel de acceso
privado se compila en bytecode, lo que significa que esto no funcionará desde Java. Por cierto, debido a la reificación en el anuncio de Kotlin
"100% interoperable con Java y Android" , se obtiene al menos inexactitud.

¿Quizás después de todo el 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); } }
En general, todo es simple con init: esta es una función en línea normal, que funciona
antes de llamar al código del propio constructor.
clase de datos
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, no quería mencionar las clases de citas, sobre las cuales ya se ha dicho mucho, pero sin embargo, hay un par de puntos dignos de atención. En primer lugar, vale la pena señalar que solo las variables que se pasaron al constructor entran en equals / hashCode / copy / toString. A la pregunta de por qué esto es así, Andrei Breslav respondió que tomar campos que no se transfirieron en el constructor también es difícil y difícil. Por cierto, es imposible heredar de la fecha de clase, la verdad es solo porque
durante la herencia el código generado no sería correcto . En segundo lugar, vale la pena señalar el método component1 () para obtener el valor del campo. Se generan tantos métodos componentN () como argumentos en el constructor. Parece inútil, pero realmente lo necesita para una
declaración de desestructuración .
declaración de desestructuración
Por ejemplo, usaremos la clase de fecha del ejemplo anterior y agregaremos el siguiente 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(); } }
Por lo general, esta característica está acumulando polvo en un estante, pero a veces puede ser útil, por ejemplo, cuando se trabaja con el contenido del 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; }
La palabra clave de operador es necesaria para anular algún operador de idioma para una clase particular. Honestamente, nunca he visto a nadie usar esto, pero sin embargo existe esa oportunidad, pero no hay magia en su interior. De hecho, el compilador simplemente reemplaza al operador con la función deseada, al igual que los typealias se reemplazan con un tipo específico.
Y sí, si en este momento pensaste en lo que sucedería si redefinieras el operador de identidad (=== cual), entonces me apresuro a molestarte, este es un operador que no se puede redefinir.
clase en línea
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;
A partir de las limitaciones: puede usar solo un argumento en el constructor, sin embargo, es comprensible, dado que la clase en línea generalmente es un contenedor sobre cualquier variable. Una clase en línea puede contener métodos, pero son simplemente estáticos. También es obvio que se han agregado todos los métodos necesarios para admitir la interoperación Java.
Resumen
No olvide que, en primer lugar, el código no siempre se descompilará correctamente y, en segundo lugar, no todos los códigos se pueden descompilar. Sin embargo, la capacidad de ver el código descompilado de Kotlin en sí mismo es muy interesante y puede aclarar mucho.