Kotlin sous le capot - voir bytecode décompilé



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

image

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; } } // $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 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.

image

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); } // $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; } } } 

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; } // $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(); } } 

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; // $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); } } 

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.

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


All Articles