
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

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

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