Kotlin unter der Haube - siehe dekompilierten Bytecode



Das Anzeigen des in Java dekompilierten Kotlin-Bytecodes ist möglicherweise der beste Weg, um zu verstehen, wie er noch funktioniert und wie sich einige Sprachkonstrukte auf die Leistung auswirken. Viele Leute haben dies schon lange getan, daher ist dieser Artikel besonders für Anfänger und diejenigen relevant, die Java schon lange beherrschen und sich kürzlich für Kotlin entschieden haben.

Ich werde absichtlich ziemlich abgedroschene und bekannte Momente verpassen, da es wahrscheinlich zum hundertsten Mal keinen Sinn macht, über die Generation von Gettern / Setzern für Var und ähnliche Dinge zu schreiben. Also fangen wir an.

Wie kann dekompilierter Bytecode in Intellij Idea angezeigt werden?


Ganz einfach - öffnen Sie einfach die gewünschte Datei und wählen Sie im Menü Extras -> Kotlin -> Kotlin-Bytecode anzeigen

Bild

Klicken Sie im angezeigten Fenster einfach auf Dekompilieren



Zum Anzeigen wird die Version von Kotlin 1.3-RC verwendet.
Kommen wir nun zum Hauptteil.

Objekt


Kotlin

object Test 

Java dekompiliert

 public final class Test { public static final Test INSTANCE; static { Test var0 = new Test(); INSTANCE = var0; } } 

Ich nehme an, jeder, der sich mit Kotlin befasst, weiß, dass das Objekt einen Singleton erstellt. Es ist jedoch für jeden nicht klar, welcher Singleton genau erstellt wird und ob er threadsicher ist.

Der dekompilierte Code zeigt, dass der empfangene Singleton der eifrigen Implementierung des Singletons ähnelt. Er wird in dem Moment erstellt, in dem der Klassenladeprogramm die Klasse lädt. Ein statischer Block wird einerseits ausgeführt, wenn er von einem Klassentreiber geladen wird, der an sich threadsicher ist. Wenn es jedoch mehr als einen Klassenraumfahrer gibt, können Sie nicht mit einer Kopie aussteigen.

Erweiterungen


Kotlin

 fun String.getEmpty(): String { return "" } 

Java dekompiliert

 public final class TestKt { @NotNull public static final String getEmpty(@NotNull String $receiver) { Intrinsics.checkParameterIsNotNull($receiver, "receiver$0"); return ""; } } 

Hier ist im Allgemeinen alles klar - Erweiterungen sind nur syntaktischer Zucker und werden zu einer regulären statischen Methode kompiliert.

Wenn jemand durch die Zeile mit Intrinsics.checkParameterIsNotNull verwechselt wurde, ist dort alles transparent - in allen Funktionen mit nicht nullbaren Argumenten fügt Kotlin eine Nullprüfung hinzu und löst eine Ausnahme aus, wenn Sie ein Nullschwein verschoben haben , obwohl in den Argumenten, die Sie versprochen haben, dies nicht zu tun. Es sieht so aus:

 public static void checkParameterIsNotNull(Object value, String paramName) { if (value == null) { throwParameterIsNullException(paramName); } } 

Was ist typisch, wenn Sie keine Funktion, sondern eine Erweiterungseigenschaft schreiben

 val String.empty: String get() { return "" } 

Als Ergebnis erhalten wir genau das, was wir für die String.getEmpty () -Methode erhalten haben

Inline


Kotlin

 inline fun something() { println("hello") } class Test { fun test() { something() } } 

Java dekompiliert

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

Mit Inline ist alles ganz einfach - eine als Inline gekennzeichnete Funktion wird einfach vollständig und vollständig an der Stelle eingefügt, an der sie aufgerufen wurde. Interessanterweise kompiliert es sich auch in Statik, wahrscheinlich für die Interoperabilität mit Java.

Die ganze Kraft der Inline wird in dem Moment offenbart, in dem Lambda in den Argumenten erscheint:

Kotlin

 inline fun something(action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } } 

Java dekompiliert

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

Im unteren Teil ist die Statik wieder sichtbar, und im oberen Teil ist klar, dass das Lambda im Funktionsargument ebenfalls inline ist und keine zusätzliche anonyme Klasse erstellt, wie dies beim üblichen Lambda in Kotlin der Fall ist.

Um dieses herum enden viele von Kotlins Inline-Kenntnissen, aber es gibt zwei weitere interessante Punkte, nämlich Noinline und Crossinline. Dies sind Schlüsselwörter, die einem Lambda zugewiesen werden können, das ein Argument in einer Inline-Funktion ist.

Kotlin

 inline fun something(noinline action: () -> Unit) { action() println("world") } class Test { fun test() { something { println("hello") } } } 

Java dekompiliert
 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); } } 

Mit einem solchen Datensatz zeigt die IDE an, dass eine solche Inline etwas weniger als vollständig nutzlos ist. Und es kompiliert genau das gleiche wie Java - erstellt Function0. Warum mit seltsamen (Function0) null.INSTANCE dekompiliert; - Ich habe keine Ahnung, höchstwahrscheinlich handelt es sich um einen Dekompiler-Fehler.

crossinline wiederum macht genau das Gleiche wie eine reguläre Inline (dh wenn im Argument nichts vor dem Lambda geschrieben wird), kann mit wenigen Ausnahmen keine Rückgabe in das Lambda geschrieben werden, was erforderlich ist, um die Fähigkeit zu blockieren, die Funktion, die Inline aufruft, plötzlich zu beenden. In dem Sinne können Sie etwas schreiben, aber erstens schwört die IDE, und zweitens erhalten wir beim Kompilieren
'return' ist hier nicht erlaubt
Der Crossinline-Bytecode unterscheidet sich jedoch nicht vom Standard-Inline-Code. Das Schlüsselwort wird nur vom Compiler verwendet.

Infix


Kotlin

 infix fun Int.plus(value: Int): Int { return this+value } class Test { fun test() { val result = 5 plus 3 } } 

Java dekompiliert

 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; } } 

Infix-Funktionen werden wie Erweiterungen der regulären Statik kompiliert

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 dekompiliert

 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 ist eine ziemlich unterhaltsame Sache. Wie Sie dem Code entnehmen können, geht die Rekursion einfach in einen viel weniger lesbaren Zyklus über, aber der Entwickler kann ruhig schlafen, da im unangenehmsten Moment nichts aus Stackoverflow herausfliegt. Eine andere Sache im wirklichen Leben ist selten, tailrec zu finden.

reifiziert


Kotlin

 inline fun <reified T>something(value: Class<T>) { println(value.simpleName) } 

Java dekompiliert

 public final class TestKt { private static final void something(Class value) { String var2 = value.getSimpleName(); System.out.println(var2); } } 

Im Allgemeinen können Sie über das Konzept von Reified selbst und warum es notwendig ist, einen ganzen Artikel schreiben. Kurz gesagt, der Zugriff auf den Typ selbst in Java ist zur Kompilierungszeit nicht möglich, da Vor dem Kompilieren von Java weiß niemand, was überhaupt da sein wird. Kotlin ist eine andere Sache. Das verifizierte Schlüsselwort kann nur in Inline-Funktionen verwendet werden, die, wie bereits erwähnt, einfach kopiert und an die richtigen Stellen eingefügt werden. Somit weiß der Compiler bereits beim „Aufruf“ der Funktion, um welchen Typ es sich handelt, und kann den Bytecode ändern.

Sie sollten darauf achten, dass eine statische Funktion mit einer privaten Zugriffsebene in Bytecode kompiliert wird, was bedeutet, dass dies nicht mit Java funktioniert. Übrigens wird aufgrund der in der Kotlin-Anzeige "100% interoperabel mit Java und Android" bestätigten zumindest Ungenauigkeit erhalten.

Bild

Vielleicht doch 99%?

init


Kotlin

 class Test { constructor() constructor(value: String) init { println("hello") } } 

Java dekompiliert

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

Im Allgemeinen ist mit init alles einfach - dies ist eine normale Inline-Funktion, die funktioniert, bevor der Code des Konstruktors selbst aufgerufen wird.

Datenklasse


Kotlin

 data class Test(val argumentValue: String, val argumentValue2: String) { var innerValue: Int = 0 } 

Java dekompiliert

 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; } } } 

Ehrlich gesagt wollte ich die Datumsklassen nicht erwähnen, über die bereits so viel gesagt wurde, aber dennoch gibt es einige Punkte, die Beachtung verdienen. Zunächst ist anzumerken, dass nur Variablen, die an den Konstruktor übergeben wurden, in equals / hashCode / copy / toString gelangen. Auf die Frage, warum dies so ist, antwortete Andrei Breslav, dass es auch schwierig und steil sei, Felder zu nehmen, die nicht im Konstruktor übertragen wurden. Übrigens ist es unmöglich, vom Klassendatum zu erben, die Wahrheit ist nur, weil während der Vererbung der generierte Code nicht korrekt wäre . Zweitens ist die Methode component1 () zu beachten, um den Feldwert zu erhalten. Es werden so viele componentN () -Methoden generiert, wie Argumente im Konstruktor vorhanden sind. Es sieht nutzlos aus, aber Sie brauchen es wirklich für eine Destrukturierungserklärung .

Destrukturierungserklärung


Als Beispiel verwenden wir die Datumsklasse aus dem vorherigen Beispiel und fügen den folgenden Code hinzu:

Kotlin

 class DestructuringDeclaration { fun test() { val (one, two) = Test("hello", "world") } } 

Java dekompiliert

 public final class DestructuringDeclaration { public final void test() { Test var3 = new Test("hello", "world"); String var1 = var3.component1(); String two = var3.component2(); } } 

Normalerweise sammelt diese Funktion Staub in einem Regal, aber manchmal kann sie nützlich sein, beispielsweise beim Arbeiten mit Karteninhalten.

Betreiber


Kotlin

 class Something(var likes: Int = 0) { operator fun inc() = Something(likes+1) } class Test() { fun test() { var something = Something() something++ } } 

Java dekompiliert

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

Das Operator-Schlüsselwort wird benötigt, um einen Sprachoperator für eine bestimmte Klasse zu überschreiben. Ehrlich gesagt habe ich noch nie jemanden gesehen, der dies benutzt, aber es gibt trotzdem eine solche Gelegenheit, aber es gibt keine Magie im Inneren. Tatsächlich ersetzt der Compiler den Operator einfach durch die gewünschte Funktion, ähnlich wie typealias durch einen bestimmten Typ ersetzt wird.
Und ja, wenn Sie gerade darüber nachgedacht haben, was passieren würde, wenn Sie den Identitätsoperator neu definieren (=== welcher), dann beeile ich mich, Sie zu verärgern. Dies ist ein Operator, der nicht neu definiert werden kann.

Inline-Klasse


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 dekompiliert

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

Aufgrund der Einschränkungen können Sie im Konstruktor nur ein Argument verwenden. Dies ist jedoch verständlich, da die Inline-Klasse im Allgemeinen ein Wrapper für eine Variable ist. Eine Inline-Klasse kann Methoden enthalten, diese sind jedoch nur statisch. Es ist auch offensichtlich, dass alle erforderlichen Methoden hinzugefügt wurden, um die Java-Interoperation zu unterstützen.

Zusammenfassung


Vergessen Sie nicht, dass erstens der Code nicht immer korrekt dekompiliert wird und zweitens nicht jeder Code dekompiliert werden kann. Die Möglichkeit, Kotlin dekompilierten Code an sich zu sehen, ist jedoch sehr interessant und kann viel verdeutlichen.

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


All Articles