
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

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

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