Kotlin di bawah tenda - lihat bytecode yang didekompilasi



Melihat bytecode Kotlin yang didekompilasi di Jawa mungkin merupakan cara terbaik untuk memahami cara kerjanya dan bagaimana beberapa konstruksi bahasa mempengaruhi kinerja. Banyak yang telah melakukan ini sendiri sejak lama, jadi artikel ini akan sangat relevan untuk pemula dan mereka yang telah lama menguasai Jawa dan memutuskan untuk menggunakan Kotlin baru-baru ini.

Saya secara khusus merindukan saat-saat yang cukup basi dan terkenal karena, mungkin, tidak masuk akal untuk keseratus kalinya menulis tentang generasi getter / setter untuk var dan hal-hal serupa. Jadi mari kita mulai.

Bagaimana cara melihat bytecode yang didekompilasi dalam Intellij Idea?


Cukup sederhana - cukup buka file yang diinginkan dan pilih di menu Tools -> Kotlin -> Show Kotlin Bytecode

gambar

Selanjutnya, di jendela yang muncul, klik saja Decompile



Untuk menonton, versi Kotlin 1.3-RC akan digunakan.
Sekarang, akhirnya, mari kita beralih ke bagian utama.

objek


Kotlin

object Test 

Java terdekompilasi

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

Saya kira semua orang yang berurusan dengan Kotlin tahu bahwa objek menciptakan singleton. Namun, itu jauh dari jelas bagi semua orang yang singleton dibuat dan apakah itu aman.

Kode yang didekompilasi menunjukkan bahwa singleton yang diterima mirip dengan implementasi singleton yang bersemangat, itu dibuat pada saat ketika classloader memuat kelas. Di satu sisi, blok statis dijalankan ketika dimuat oleh pengandar kelas, yang dengan sendirinya aman. Di sisi lain, jika ada lebih dari satu pengemudi kelas, maka Anda tidak dapat turun dengan satu salinan.

ekstensi


Kotlin

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

Java terdekompilasi

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

Di sini, secara umum, semuanya jelas - ekstensi hanyalah gula sintaksis dan dikompilasi menjadi metode statis biasa.

Jika seseorang bingung oleh baris dengan Intrinsics.checkParameterIsNotNull, maka semuanya transparan di sana - dalam semua fungsi dengan argumen yang tidak dapat dibatalkan, Kotlin menambahkan cek nol dan melempar pengecualian jika Anda memasukkan babi null, meskipun Anda berjanji untuk tidak melakukannya dalam argumen. Ini terlihat seperti ini:

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

Apa yang khas jika Anda menulis bukan fungsi, tetapi properti ekstensi

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

Kemudian, sebagai hasilnya, kita mendapatkan hal yang persis sama dengan yang kita dapatkan untuk metode String.getEmpty ()

sebaris


Kotlin

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

Java terdekompilasi

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

Dengan inline, semuanya cukup sederhana - fungsi yang ditandai sebagai inline secara lengkap dan sepenuhnya dimasukkan ke tempat dari mana ia dipanggil. Menariknya, ia juga mengkompilasi dirinya menjadi statika, mungkin untuk interoperabilitas dengan Java.

Semua kekuatan inline diungkapkan pada saat ketika lambda muncul dalam argumen:

Kotlin

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

Java terdekompilasi

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

Di bagian bawah, statika sekali lagi terlihat, dan di bagian atas jelas bahwa lambda dalam argumen fungsi juga diuraikan, dan tidak membuat kelas anonim tambahan, seperti halnya dengan lambda yang biasa di Kotlin.

Di sekitar ini, banyak pengetahuan inline Kotlin berakhir, tetapi ada 2 poin lebih menarik, yaitu noinline dan crossinline. Ini adalah kata kunci yang dapat ditugaskan ke lambda yang merupakan argumen dalam fungsi sebaris.

Kotlin

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

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

Dengan catatan seperti itu, IDE mulai menunjukkan bahwa inline semacam itu tidak berguna sedikit pun. Dan itu mengkompilasi persis sama dengan Java - menciptakan Function0. Mengapa didekompilasi dengan aneh (Function0) null.INSTANCE; - Saya tidak tahu, kemungkinan besar ini adalah bug decompiler.

crossinline, pada gilirannya, melakukan hal yang persis sama dengan inline reguler (yaitu, jika tidak ada yang ditulis sebelum lambda dalam argumen), dengan beberapa pengecualian, return tidak dapat ditulis dalam lambda, yang diperlukan untuk memblokir kemampuan untuk tiba-tiba mengakhiri fungsi yang memanggil inline. Dalam arti, Anda dapat menulis sesuatu, tetapi pertama-tama, IDE akan bersumpah, dan kedua, ketika kompilasi, kita dapatkan
'kembali' tidak diperbolehkan di sini
Namun, bytecode crossinline tidak berbeda dari inline default - kata kunci hanya digunakan oleh kompiler.

infiks


Kotlin

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

Java terdekompilasi

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

Fungsi Infix dikompilasi seperti ekstensi ke statika biasa

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 terdekompilasi

 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 adalah hal yang agak menghibur. Seperti yang dapat Anda lihat dari kode, rekursi masuk ke siklus yang jauh lebih mudah dibaca, tetapi pengembang dapat tidur nyenyak, karena tidak ada yang akan terbang keluar dari Stackoverflow pada saat yang paling tidak menyenangkan. Hal lain dalam kehidupan nyata adalah jarang menemukan tailrec.

reified


Kotlin

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

Java terdekompilasi

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

Secara umum, tentang konsep reified itu sendiri dan mengapa perlu, Anda dapat menulis seluruh artikel. Singkatnya, akses ke tipe itu sendiri di Jawa tidak mungkin pada waktu kompilasi, karena Sebelum mengkompilasi Java, tidak ada yang tahu apa yang akan ada di sana. Kotlin adalah masalah lain. Kata kunci terverifikasi hanya dapat digunakan dalam fungsi inline, yang, sebagaimana telah dicatat, disalin dan ditempelkan ke tempat yang tepat, sehingga sudah selama "panggilan" fungsi, kompiler sudah mengetahui jenisnya dan dapat memodifikasi bytecode.

Anda harus memperhatikan fakta bahwa fungsi statis dengan tingkat akses pribadi dikompilasi dalam bytecode, yang berarti bahwa ini tidak akan berhasil di Jawa. Ngomong-ngomong, karena terungkap dalam iklan Kotlin "100% dapat dioperasikan dengan Java dan Android" , setidaknya ketidaktepatan diperoleh.

gambar

Mungkin setelah semua 99%?

init


Kotlin

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

Java terdekompilasi

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

Secara umum, dengan init, semuanya sederhana - ini adalah fungsi inline normal, yang berfungsi sebelum memanggil kode konstruktor itu sendiri.

kelas data


Kotlin

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

Java terdekompilasi

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

Sejujurnya, saya tidak ingin menyebutkan kelas kencan, tentang yang sudah banyak dikatakan, tetapi ada beberapa hal yang patut diperhatikan. Pertama-tama, perlu dicatat bahwa hanya variabel yang diteruskan ke konstruktor yang sama dengan / hashCode / copy / toString. Untuk pertanyaan mengapa demikian, Andrei Breslav menjawab bahwa mengambil bidang yang tidak ditransfer dalam konstruktor juga sulit dan sulit. By the way, tidak mungkin untuk mewarisi dari tanggal kelas, kebenarannya hanya karena selama pewarisan kode yang dihasilkan tidak akan benar . Kedua, perlu dicatat metode component1 () untuk mendapatkan nilai bidang. Karena banyak metode componentN () dihasilkan karena ada argumen dalam konstruktor. Itu terlihat tidak berguna, tetapi Anda benar-benar membutuhkannya untuk deklarasi destrukturisasi .

deklarasi perusakan


Sebagai contoh, kita akan menggunakan kelas tanggal dari contoh sebelumnya dan menambahkan kode berikut:

Kotlin

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

Java terdekompilasi

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

Biasanya fitur ini mengumpulkan debu di rak, tetapi kadang-kadang bisa bermanfaat, misalnya, ketika bekerja dengan konten peta.

operator


Kotlin

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

Java terdekompilasi

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

Kata kunci operator diperlukan untuk mengganti beberapa operator bahasa untuk kelas tertentu. Sejujurnya, saya belum pernah melihat orang menggunakan ini, tetapi meskipun demikian ada kesempatan seperti itu, tetapi tidak ada keajaiban di dalamnya. Bahkan, kompiler hanya menggantikan operator dengan fungsi yang diinginkan, seperti typealias diganti dengan tipe tertentu.
Dan ya, jika sekarang Anda berpikir tentang apa yang akan terjadi jika Anda mendefinisikan ulang operator identitas (=== yang), maka saya buru-buru membuat Anda marah, ini adalah operator yang tidak dapat didefinisikan ulang.

kelas inline


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 terdekompilasi

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

Dari batasan - Anda hanya dapat menggunakan satu argumen dalam konstruktor, namun, dapat dimengerti, mengingat bahwa kelas inline umumnya adalah pembungkus atas salah satu variabel. Kelas inline mungkin berisi metode, tetapi mereka hanya statis. Juga jelas bahwa semua metode yang diperlukan telah ditambahkan untuk mendukung interoperasi Java.

Ringkasan


Jangan lupa bahwa, pertama, kode tidak akan selalu didekompilasi dengan benar, dan kedua, tidak setiap kode dapat didekompilasi. Namun, kemampuan untuk menonton kode dekompilasi Kotlin itu sendiri sangat menarik dan dapat mengklarifikasi banyak.

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


All Articles