
La visualisation du bytecode Kotlin décompilé en Java est peut-être le meilleur moyen de comprendre comment il fonctionne toujours et comment certaines constructions de langage affectent les performances. Beaucoup l'ont fait eux-mêmes il y a longtemps, donc cet article sera particulièrement pertinent pour les débutants et ceux qui maîtrisent depuis longtemps Java et ont décidé d'utiliser Kotlin récemment.
Je manque spécifiquement des moments assez galvaudés et bien connus car, probablement, cela n'a aucun sens pour la centième fois d'écrire sur la génération des getters / setters pour var et des choses similaires. Commençons donc.
Comment afficher le bytecode décompilé dans Intellij Idea?
Assez simple - ouvrez simplement le fichier souhaité et sélectionnez dans le menu Outils -> Kotlin -> Afficher le bytecode Kotlin

Ensuite, dans la fenêtre qui apparaît, cliquez simplement sur Décompiler

Pour la visualisation, la version de Kotlin 1.3-RC sera utilisée.
Maintenant, enfin, passons à la partie principale.
objet
Kotlin
object Test
Java décompilé
public final class Test { public static final Test INSTANCE; static { Test var0 = new Test(); INSTANCE = var0; } }
Je suppose que tous ceux qui traitent avec Kotlin savent que cet objet crée un singleton. Cependant, il est loin d'être évident pour tout le monde exactement quel singleton est créé et s'il est thread-safe.
Le code décompilé montre que le singleton reçu est similaire à l'implémentation désirée du singleton, il est créé au moment où le chargeur de classe charge la classe. D'une part, un bloc statique est exécuté lorsqu'il est chargé par un pilote de classe, qui en lui-même est thread-safe. D'un autre côté, s'il y a plusieurs chauffeurs de classe, vous ne pouvez pas en descendre avec une seule copie.
extensions
Kotlin
fun String.getEmpty(): String { return "" }
Java décompilé
public final class TestKt { @NotNull public static final String getEmpty(@NotNull String $receiver) { Intrinsics.checkParameterIsNotNull($receiver, "receiver$0"); return ""; } }
Ici, en général, tout est clair - les extensions ne sont que du sucre syntaxique et compilées dans une méthode statique régulière.
Si quelqu'un a été confondu par la ligne avec Intrinsics.checkParameterIsNotNull, alors tout y est transparent - dans toutes les fonctions avec des arguments non nullables, Kotlin ajoute une vérification nulle et lève une exception si vous avez glissé un
cochon nul, bien que vous ayez promis de ne pas le faire dans les arguments. Cela ressemble à ceci:
public static void checkParameterIsNotNull(Object value, String paramName) { if (value == null) { throwParameterIsNullException(paramName); } }
Ce qui est typique si vous n'écrivez pas une fonction, mais une propriété d'extension
val String.empty: String get() { return "" }
Ensuite, en conséquence, nous obtenons exactement la même chose que celle que nous avons obtenue pour la méthode String.getEmpty ()
en ligne
Kotlin
inline fun something() { println("hello") } class Test { fun test() { something() } }
Java décompilé
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); } }
Avec inline, tout est assez simple - une fonction marquée comme inline est simplement complètement et complètement insérée à l'endroit d'où elle a été appelée. Fait intéressant, il se compile également en statique, probablement pour l'interopérabilité avec Java.
Toute la puissance de l'inline se révèle au moment où
lambda apparaît dans les arguments:
Kotlin
inline fun something(action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } }
Java décompilé
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); } }
Dans la partie inférieure, la statique est à nouveau visible, et dans la partie supérieure, il est clair que le lambda dans l'argument de fonction est également en ligne, et ne crée pas de classe anonyme supplémentaire, comme c'est le cas avec le lambda habituel dans Kotlin.
Autour de cela, de nombreuses connaissances en ligne de Kotlin se terminent, mais il y a 2 autres points intéressants, à savoir noinline et crossinline. Ce sont des mots clés qui peuvent être attribués à un lambda qui est un argument dans une fonction en ligne.
Kotlin
inline fun something(noinline action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } }
Java décompilé
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); } }
Avec un tel record, l'IDE commence à indiquer qu'une telle ligne est inutile un peu moins que complètement. Et il compile exactement la même chose que Java - crée Function0. Pourquoi décompilé avec bizarre (Function0) null.INSTANCE; - Je n'en ai aucune idée, il s'agit très probablement d'un bug du décompilateur.
crossinline, à son tour, fait exactement la même chose qu'une ligne régulière (c'est-à-dire, si rien n'est écrit avant le lambda dans l'argument), à quelques exceptions près, le retour ne peut pas être écrit dans le lambda, ce qui est nécessaire pour bloquer la possibilité de mettre fin soudainement à la fonction qui appelle en ligne. En ce sens, vous pouvez écrire quelque chose, mais d'une part, l'IDE jure, et d'autre part, lors de la compilation, nous obtenons
le «retour» n'est pas autorisé ici
Cependant, le bytecode crossinline ne diffère pas de l'inline par défaut - le mot-clé est utilisé uniquement par le compilateur.
infixer
Kotlin
infix fun Int.plus(value: Int): Int { return this+value } class Test { fun test() { val result = 5 plus 3 } }
Java décompilé
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; } }
Les fonctions Infix sont compilées comme des extensions de la statique régulière
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 décompilé
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 est une chose plutôt divertissante. Comme vous pouvez le voir dans le code, la récursivité entre simplement dans un cycle beaucoup moins lisible, mais le développeur peut dormir paisiblement, car rien ne sortira de Stackoverflow au moment le plus désagréable. Une autre chose dans la vraie vie est rarement de trouver un tailrec.
réifié
Kotlin
inline fun <reified T>something(value: Class<T>) { println(value.simpleName) }
Java décompilé
public final class TestKt { private static final void something(Class value) { String var2 = value.getSimpleName(); System.out.println(var2); } }
En général, sur le concept de réifié lui-même et pourquoi il est nécessaire, vous pouvez écrire un article entier. En bref, l'accès au type lui-même en Java n'est pas possible au moment de la compilation, car Avant de compiler Java, personne ne sait ce qu'il y aura du tout. Kotlin est une autre affaire. Le mot clé réifié ne peut être utilisé que dans les fonctions en ligne, qui, comme déjà noté, sont simplement copiées et collées aux bons endroits, donc déjà pendant «l'appel» de la fonction, le compilateur est
déjà conscient de son type et peut modifier le bytecode.
Vous devez faire attention au fait qu'une fonction statique avec un niveau d'accès
privé est compilée en bytecode, ce qui signifie que cela ne fonctionnera pas en Java. Soit dit en passant, en raison de réifié dans la publicité Kotlin
«100% interopérable avec Java et Android» , au moins l'inexactitude est obtenue.

Peut-être après tout 99%?
init
Kotlin
class Test { constructor() constructor(value: String) init { println("hello") } }
Java décompilé
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 général, avec init, tout est simple - il s'agit d'une fonction en ligne normale, qui fonctionne
avant d' appeler le code du constructeur lui-même.
classe de données
Kotlin
data class Test(val argumentValue: String, val argumentValue2: String) { var innerValue: Int = 0 }
Java décompilé
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); }
Honnêtement, je ne voulais pas mentionner les classes de dates, dont on a déjà tant parlé, mais néanmoins il y a quelques points dignes d'attention. Tout d'abord, il convient de noter que seules les variables transmises au constructeur entrent dans equals / hashCode / copy / toString. À la question de savoir pourquoi il en est ainsi, Andrei Breslav a répondu que la prise de champs qui n'avaient pas été transférés dans le constructeur est également difficile et difficile. Soit dit en passant, il est impossible d'hériter de la date de classe, la vérité est seulement parce que
pendant l'héritage le code généré ne serait pas correct . Deuxièmement, il convient de noter la méthode component1 () pour obtenir la valeur du champ. Autant de méthodes componentN () sont générées qu'il y a d'arguments dans le constructeur. Cela semble inutile, mais vous en avez vraiment besoin pour une
déclaration de déstructuration .
déclaration de déstructuration
Pour un exemple, nous utiliserons la classe de date de l'exemple précédent et ajouterons le code suivant:
Kotlin
class DestructuringDeclaration { fun test() { val (one, two) = Test("hello", "world") } }
Java décompilé
public final class DestructuringDeclaration { public final void test() { Test var3 = new Test("hello", "world"); String var1 = var3.component1(); String two = var3.component2(); } }
Habituellement, cette fonctionnalité accumule de la poussière sur une étagère, mais elle peut parfois être utile, par exemple, lorsque vous travaillez avec le contenu d'une carte.
opérateur
Kotlin
class Something(var likes: Int = 0) { operator fun inc() = Something(likes+1) } class Test() { fun test() { var something = Something() something++ } }
Java décompilé
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; }
Le mot clé operator est nécessaire pour remplacer un opérateur de langage pour une classe particulière. Honnêtement, je n'ai jamais vu personne utiliser cela, mais néanmoins il y a une telle opportunité, mais il n'y a pas de magie à l'intérieur. En fait, le compilateur remplace simplement l'opérateur par la fonction souhaitée, tout comme les typealias sont remplacés par un type spécifique.
Et oui, si en ce moment vous pensiez à ce qui se passerait si vous redéfinissez l'opérateur d'identité (=== qui), alors je m'empresse de vous déranger, c'est un opérateur qui ne peut pas être redéfini.
classe en ligne
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 décompilé
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;
D'après les limitations - vous ne pouvez utiliser qu'un seul argument dans le constructeur, cependant, cela est compréhensible, étant donné que la classe inline est généralement un wrapper sur n'importe quelle variable. Une classe en ligne peut contenir des méthodes, mais elles sont simplement statiques. Il est également évident que toutes les méthodes nécessaires ont été ajoutées pour prendre en charge l'interopérabilité Java.
Résumé
N'oubliez pas que, d'une part, le code ne sera pas toujours décompilé correctement, et d'autre part, tous les codes ne peuvent pas être décompilés. Cependant, la possibilité de regarder le code décompilé Kotlin en soi est très intéressante et peut clarifier beaucoup.