Menulis Kode Java Ramah Kotlin

Dari luar, sepertinya Kotlin telah menyederhanakan pengembangan Android tanpa memperkenalkan kesulitan baru sama sekali: bahasanya kompatibel dengan Java, sehingga bahkan proyek Java yang besar dapat secara bertahap diterjemahkan ke dalamnya tanpa mengganggu siapa pun, bukan? Tetapi jika Anda melihat lebih dalam, di setiap kotak ada bagian bawah ganda, dan di meja rias ada pintu rahasia. Bahasa pemrograman adalah proyek yang terlalu rumit untuk digabungkan tanpa nuansa rumit.

Tentu saja, ini tidak berarti "semuanya buruk dan Anda tidak perlu menggunakan Kotlin dengan Java," tetapi itu berarti bahwa Anda harus tahu tentang nuansa dan mempertimbangkannya. Pada konferensi Mobius kami, Sergei Ryabov berbicara tentang cara menulis kode di Kotlin yang mudah diakses dari Jawa. Dan para penonton sangat menyukai laporan itu sehingga kami tidak hanya memutuskan untuk mengirim video, tetapi juga membuat versi teks untuk Habr:


Saya telah menulis Kotlin selama lebih dari tiga tahun, sekarang hanya di atasnya, tetapi pada awalnya saya menyeret Kotlin ke proyek Jawa yang ada. Oleh karena itu, pertanyaan "bagaimana cara menyatukan Jawa dan Kotlin" dalam cara saya muncul cukup sering.

Seringkali ketika Anda menambahkan Kotlin ke proyek, Anda dapat melihat bagaimana ini ...

compile 'rxbinding:xyx' compile 'rxbinding-appcompat-v7:xyx' compile 'rxbinding-design:xyx' compile 'autodispose:xyz' compile 'autodispose-android:xyz' compile 'autodispose-android-archcomponents:xyz' 

... berubah menjadi ini:

 compile 'rxbinding:xyx' compile 'rxbinding-kotlin:xyx' compile 'rxbinding-appcompat-v7:xyx' compile 'rxbinding-appcompat-v7-kotlin:xyx' compile 'rxbinding-design:xyx' compile 'rxbinding-design-kotlin:xyx' compile 'autodispose:xyz' compile 'autodispose-kotlin:xyz' compile 'autodispose-android:xyz' compile 'autodispose-android-kotlin:xyz' compile 'autodispose-android-archcomponents:xyz' compile 'autodispose-android-archcomponents-kotlin:xyz' 

Kekhasan beberapa tahun terakhir: perpustakaan paling populer mendapatkan pembungkus sehingga dapat digunakan dari Kotlin secara lebih idiomatis.

Jika Anda menulis di Kotlin, maka Anda tahu bahwa ada fungsi ekstensi keren, fungsi inline, ekspresi lambda yang tersedia dari Java 6. Dan ini keren, itu menarik kita ke Kotlin, tetapi muncul pertanyaan. Salah satu fitur bahasa terbesar dan paling dipublikasikan adalah interoperabilitas dengan Java. Jika Anda memperhitungkan semua fitur yang terdaftar, lalu mengapa tidak menulis perpustakaan di Kotlin saja? Mereka semua akan bekerja dengan baik di luar kotak dengan Java, dan Anda tidak perlu mendukung semua pembungkus ini, semua orang akan senang dan puas.

Tapi, tentu saja, dalam praktiknya, tidak semuanya semarak di brosur, selalu ada "atribut font kecil", ada tepi tajam di persimpangan Kotlin dan Jawa, dan hari ini kita akan membicarakan hal ini sedikit.

Tepi yang tajam


Mari kita mulai dengan perbedaannya. Misalnya, apakah Anda tahu bahwa di Kotlin tidak ada kata kunci yang mudah menguap, disinkronkan, strictfp, sementara? Mereka digantikan oleh anotasi dengan nama yang sama yang terletak di paket kotlin.jvm. Jadi, sebagian besar percakapan akan membahas tentang isi paket ini.

Ada Timber - semacam perpustakaan-abstraksi atas penebang dari Zheka Vartanov yang terkenal kejam . Ini memungkinkan Anda untuk menggunakannya di mana-mana dalam aplikasi Anda, dan segala sesuatu di mana Anda ingin mengirim log (ke logcat, atau ke server Anda untuk analisis, atau pelaporan kerusakan, dan sebagainya) berubah menjadi plug-in.

Mari kita bayangkan misalnya kita ingin menulis perpustakaan yang sama, hanya untuk analitik. Juga melepaskan diri.

 object Analytics { fun send(event: Event) {} fun addPlugins(plugs: List<Plugin>) {} fun getPlugins(): List<Plugin> {} } interface Plugin { fun init() fun send(event: Event) fun close() } data class Event( val name: String, val context: Map<String, Any> = emptyMap() ) 

Kami mengambil pola konstruksi yang sama, kami memiliki satu titik masuk - ini adalah Analytics. Kami dapat mengirim acara di sana, menambahkan plugin, dan melihat apa yang telah kami tambahkan di sana.

Plugin adalah antarmuka plugin yang mengabstraksi API analitik tertentu.

Dan, pada kenyataannya, kelas Peristiwa berisi kunci dan atribut kami yang kami kirim. Di sini laporannya bukan tentang apakah layak menggunakan singletones, jadi mari kita tidak membiakkan holivar, tapi kita akan melihat bagaimana menyisir semua ini.

Sekarang sedikit menyelam. Berikut adalah contoh menggunakan perpustakaan kami di Kotlin:

 private fun useAnalytics() { Analytics.send(Event("only_name_event")) val props = mapOf( USER_ID to 1235, "my_custom_attr" to true ) Analytics.send(Event("custom_event", props)) val hasPlugins = Analytics.hasPlugins Analytics.addPlugin(EMPTY_PLUGIN) // dry-run Analytics.addPlugins(listOf(LoggerPlugin("ALog"), SegmentPlugin))) val plugins = Analytics.getPlugins() // ... } 

Pada prinsipnya, ini terlihat seperti yang diharapkan. Satu titik masuk, metode disebut a la statika. Acara tanpa parameter, acara dengan atribut. Kami memeriksa untuk melihat apakah kami memiliki plugin, dorong plugin kosong di sana untuk hanya membuat semacam "lari kering" berjalan. Atau tambahkan beberapa plugin lain, tampilkan, dan sebagainya. Secara umum, kasus pengguna standar, saya harap semuanya jelas sejauh ini.

Sekarang mari kita lihat apa yang terjadi di Jawa ketika kita melakukan hal yang sama:

 private static void useAnalytics() { Analytics.INSTANCE.send(new Event("only_name_event", Collections.emptyMap())); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.INSTANCE.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.INSTANCE.getHasPlugins(); Analytics.INSTANCE.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN()); // dry-run final List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("ALog"), new SegmentPlugin()); // ... } 

Keributan dengan INSTANCE segera mengalir ke mata saya, yang membentang, kehadiran nilai eksplisit untuk parameter default dengan atribut, beberapa getter dengan nama bodoh. Karena kami, secara umum, telah berkumpul di sini untuk mengubah ini menjadi sesuatu yang mirip dengan file sebelumnya dengan Kotlin, maka mari kita lalui setiap saat yang tidak kita sukai dan cobalah untuk mengadaptasinya.

Mari kita mulai dengan Event. Kami menghapus parameter Colletions.emptyMap () dari baris kedua, dan kesalahan kompilator muncul. Apa alasannya?

 data class Event( val name: String, val context: Map<String, Any> = emptyMap() ) 

Konstruktor kami memiliki parameter default yang kami berikan nilainya. Kami datang dari Jawa ke Kotlin, logis untuk mengasumsikan bahwa keberadaan parameter default menghasilkan dua konstruktor: satu penuh dengan dua parameter, dan satu parsial, yang hanya nama yang dapat ditentukan. Jelas, kompiler tidak berpikir begitu. Mari kita lihat mengapa dia pikir kita salah.

Alat utama kami untuk menganalisis semua tikungan dan belokan tentang bagaimana Kotlin berubah menjadi bytecode JVM - Kotlin Bytecode Viewer. Di Android Studio dan IntelliJ IDEA, terletak di menu Tools - Kotlin - Show Kotlin Bytecode. Anda cukup menekan Cmd + Shift + A dan ketik Kotlin Bytecode ke dalam bilah pencarian.



Di sini, secara mengejutkan, kita melihat bytecode dari apa yang menjadi kelas Kotlin kita. Saya tidak berharap Anda memiliki pengetahuan yang baik tentang bytecode, dan yang paling penting, pengembang IDE juga tidak mengharapkannya. Karena itu, mereka membuat tombol Decompile.

Setelah mengkliknya, kita melihat kode Java yang kira-kira bagus:

 public final class Event { @NotNull private final String name; @NotNull private final Map context; @NotNull public final String getName() { return this.name; } @NotNull public final Map getContext() { return this.context; } public Event(@NotNull String name, @NotNull Map context) { Intrinsics.checkParameterIsNotNull(name, "name"); Intrinsics.checkParameterIsNotNull(context, "context"); super(); this.name = name; this.context = context; } // $FF: Synthetic method public Event(String var1, Map var2, int var3, DefaultConstructorMarker var4) { if ((var3 & 2) != 0) { var2 = MapsKt.emptyMap(); } // ... } 

Kami melihat bidang kami, getter, konstruktor yang diharapkan dengan dua nama parameter dan konteks, semuanya terjadi dengan baik. Dan di bawah ini kita melihat konstruktor kedua, dan ini dia dengan tanda tangan yang tidak terduga: tidak dengan satu parameter, tetapi untuk beberapa alasan dengan empat.

Di sini Anda bisa merasa malu, tetapi Anda bisa naik sedikit lebih dalam dan mencari-cari. Mulai memahami, kita akan memahami bahwa DefaultConstructorMarker adalah kelas privat dari pustaka standar Kotlin, ditambahkan di sini sehingga tidak ada konflik dengan konstruktor tertulis kami, karena kami tidak dapat menetapkan parameter tipe DefaultConstructorMarker dengan tangan kami. Dan hal yang menarik tentang int var3 adalah bit mask dari nilai default apa yang harus kita gunakan. Dalam kasus ini, jika bitmask cocok dengan keduanya, kita tahu bahwa var2 tidak disetel, atribut kita tidak disetel, dan kita menggunakan nilai default.

Bagaimana kita dapat memperbaiki situasi? Untuk melakukan ini, ada anotasi ajaib @JvmOverloads dari paket yang sudah saya bicarakan. Kita harus menggantungnya di konstruktor.

 data class Event @JvmOverloads constructor( val name: String, val context: Map<String, Any> = emptyMap() ) 

Dan apa yang akan dia lakukan? Mari kita beralih ke alat yang sama. Sekarang kita melihat konstruktor lengkap kami, dan konstruktor dengan DefaultConstructorMarker, dan, lihatlah, konstruktor dengan satu parameter, yang sekarang tersedia dari Jawa:

 @JvmOverloads public Event(@NotNull String name) { this.name, (Map)null, 2, (DefaultConstructorMarker)null); } 

Dan, seperti yang Anda lihat, dia mendelegasikan semua pekerjaan dengan parameter default ke konstruktor kami dengan masker bit. Jadi, kami tidak menghasilkan informasi tentang nilai default apa yang perlu kami taruh di sana, kami hanya mendelegasikan semuanya menjadi satu konstruktor. Bagus Kami memeriksa apa yang kami dapatkan dari sisi Java: kompilator senang dan tidak marah.

Mari kita lihat apa yang tidak kita sukai selanjutnya. Kami tidak menyukai INSTANCE ini, yang dalam IDEA berwarna ungu. Saya tidak suka warna ungu :)



Mari kita periksa, karena apa yang terjadi. Mari kita lihat bytecode lagi.

Sebagai contoh, kami menyoroti fungsi init dan memastikan bahwa init memang dihasilkan tidak statis.



Artinya, apa pun yang dikatakan orang, kita perlu bekerja dengan instance kelas ini dan memanggil metode ini di atasnya. Tetapi kita dapat memaksa generasi semua metode ini menjadi statis. Ada penjelasan yang bagus tentang @JvmStatic untuk ini. Mari menambahkannya ke init dan mengirim fungsi dan memeriksa apa yang dipikirkan oleh kompiler tentang itu sekarang.

Kami melihat bahwa kata kunci statis telah ditambahkan ke init final publik (), dan kami telah menyelamatkan diri dari bekerja dengan INSTANCE. Kami akan memverifikasi ini dalam kode Java.

Kompiler sekarang memberitahu kita bahwa kita menggunakan metode statis dari konteks INSTANCE. Ini dapat diperbaiki: tekan Alt + Enter, pilih “Cleanup Code”, dan voila, INSTANCE menghilang, semuanya tampak sama seperti di Kotlin:

  Analytics.send(new Event("only_name_event")); 

Sekarang kami memiliki skema untuk bekerja dengan metode statis. Tambahkan anotasi ini di tempat yang penting bagi kami:



Dan komentar: jika metode yang kita miliki jelas metode instan, maka, misalnya, dengan properti, tidak semuanya jelas. Bidang itu sendiri (mis. Plugin) dihasilkan sebagai statis. Tetapi getter dan setter berfungsi sebagai metode instan. Karena itu, untuk properti, Anda juga perlu menambahkan anotasi ini untuk menjadikan setter dan getter sebagai statis. Sebagai contoh, kita melihat variabel isInited, menambahkan anotasi @JvmStatic ke dalamnya, dan sekarang kita melihat di Kotlin Bytecode Viewer bahwa metode isInited () telah menjadi statis, semuanya baik-baik saja.

Sekarang mari kita pergi ke kode Java, "untuk membersihkan" itu, dan semuanya terlihat seperti Kotlin, kecuali untuk titik koma dan kata baru - yah, Anda tidak akan menyingkirkannya.

 public static void useAnalytics() { Analytics.send(new Event("only_name_event")); final Map<String, Object> props = new HashMap<>(); props.put(USER_ID, 1235); props.put("my_custom_attr", true); Analytics.send(new Event("custom_event", props)); boolean hasPlugins = Analytics.getHasPlugins(); Analytics.addPlugin(Analytics.INSTANCE.getEMPTY_PLUGIN()); // dry-run // ... } 

Langkah selanjutnya: kita melihat pengambil getHasPlugins dengan nama bodoh ini memakai dua awalan sekaligus. Tentu saja, saya bukan ahli bahasa Inggris yang hebat, tetapi bagi saya sepertinya ada hal lain yang tersirat di sini. Mengapa ini terjadi?

Seperti yang mereka ketahui dekat dengan Kotlin, untuk nama properti untuk getter dan setter dihasilkan sesuai dengan aturan JavaBeans. Ini berarti bahwa getter umumnya akan mendapatkan prefix, setter dengan set prefix. Tetapi ada satu pengecualian: jika Anda memiliki bidang Boolean dan namanya memiliki awalannya, maka pengambil akan diawali dengan is. Ini dapat dilihat pada contoh bidang isInited di atas.

Sayangnya, jauh dari selalu bidang Boolean harus dipanggil melalui ini. isPlugins tidak akan cukup memuaskan apa yang ingin kita perlihatkan secara semantik dengan namanya. Bagaimana kita?

Dan itu tidak sulit bagi kami, karena ini ada anotasi kami sendiri (seperti yang sudah Anda pahami, saya akan sering mengulanginya hari ini). Anotasi @JvmName memungkinkan Anda menentukan nama apa pun yang kami inginkan (didukung secara alami oleh Java). Tambahkan:

 @JvmStatic val hasPlugins @JvmName("hasPlugin") get() = plugins.isNotEmpty() 

Mari kita periksa apa yang kita dapatkan di Jawa: metode getHasPlugins sudah tidak ada lagi, tetapi hasPlugins adalah sesuatu untuk dirinya sendiri. Ini menyelesaikan masalah kami, sekali lagi, dengan satu anotasi. Sekarang kami menyelesaikan semua anotasi!

Seperti yang Anda lihat, di sini kami menempatkan anotasi langsung pada pengambil. Apa alasannya? Dengan fakta bahwa di bawah properti itu banyak segalanya, dan tidak jelas apa yang berlaku untuk @JvmName. Jika Anda mentransfer anotasi ke val hasPlugins sendiri, kompiler tidak akan mengerti apa yang akan diterapkan.

Namun, Kotlin juga memiliki kemampuan untuk menentukan di mana anotasi digunakan tepat di dalamnya. Anda dapat menentukan pengambil target, seluruh file, parameter, mendelegasikan, bidang, properti, fungsi ekstensi penerima, penyetel dan parameter penyetel. Dalam kasus kami, rajin rajin menarik. Dan jika Anda suka ini, itu akan memiliki efek yang sama seperti ketika kita menggantungkan anotasi pada get:

 @get:JvmName("hasPlugins") @JvmStatic val hasPlugins get() = plugins.isNotEmpty() 

Dengan demikian, jika Anda tidak memiliki pengambil kustom, maka Anda dapat melampirkannya langsung ke properti Anda, dan semuanya akan beres.

Poin berikutnya yang sedikit membingungkan kami adalah “Analytics.INSTANCE.getEMPTY_PLUGIN ()”. Di sini masalahnya tidak lagi bahkan dalam bahasa Inggris, tetapi hanya: MENGAPA? Jawabannya hampir sama, tetapi pertama-tama perkenalan kecil.

Untuk membuat bidang konstan, Anda memiliki dua cara. Jika Anda mendefinisikan konstanta sebagai tipe primitif atau sebagai String, dan juga di dalam objek, maka Anda dapat menggunakan kata kunci const, dan kemudian pengambil-setter dan hal-hal lain tidak akan dihasilkan. Ini akan menjadi konstanta biasa - statik final privat - dan akan digariskan, yaitu, hal Jawa yang benar-benar biasa.

Tetapi jika Anda ingin membuat konstanta dari objek yang berbeda dari string, maka Anda tidak akan dapat menggunakan kata const untuk ini. Di sini kita memiliki val EMPTY_PLUGIN = EmptyPlugin (), menurutnya, bahwa pengambil yang mengerikan jelas dihasilkan. Kita dapat mengganti nama @JvmName dengan anotasi, menghapus awalan ini, tetapi tetap saja metode - dengan tanda kurung. Jadi, solusi lama tidak akan berfungsi, kami mencari yang baru.

Dan di sini untuk ini anotasi @JvmField, yang mengatakan: "Saya tidak ingin getter di sini, saya tidak ingin setter, buatkan saya bidang." Letakkan di depan val EMPTY_PLUGIN dan verifikasi bahwa semuanya benar.



Kotlin Bytecode Viewer menunjukkan bagian yang disorot di mana Anda saat ini berdiri di dalam file. Kami sekarang berdiri di EMPTY_PLUGIN, dan Anda melihat bahwa di sini semacam inisialisasi ditulis dalam konstruktor. Faktanya adalah pengambil tidak lagi ada dan akses ke sana hanya untuk merekam. Dan jika Anda mengklik mendekompilasi, kami melihat bahwa "EmptyPlugin final statis publik EMPTY_PLUGIN" telah muncul, inilah yang kami capai. Bagus Kami memeriksa bahwa semuanya menyenangkan semua orang, khususnya, kompiler. Hal terpenting yang Anda butuhkan untuk menenangkan adalah kompiler.

Generik


Mari kita istirahat dari kode dan melihat obat generik. Ini adalah topik yang cukup panas. Atau licin, siapa yang tidak menyukainya lagi. Jawa memiliki kompleksitasnya sendiri, tetapi Kotlin berbeda. Pertama-tama, kami memperhatikan variasi. Apa ini

Variabilitas adalah cara mentransfer informasi tentang hierarki jenis dari tipe dasar ke turunan, misalnya, ke wadah atau ke obat generik. Di sini kita memiliki kelas Hewan dan Anjing dengan koneksi yang sangat jelas: Anjing adalah subtipe, Hewan adalah subtipe, panah berasal dari subtipe.



Dan koneksi apa yang akan dimiliki turunannya? Mari kita lihat beberapa kasing.

Yang pertama adalah Iterator. Untuk menentukan apa itu subtipe dan apa itu subtipe, kita akan dipandu oleh aturan substitusi Barbara Liskov. Ini dapat dirumuskan sebagai berikut: "subtipe seharusnya tidak memerlukan lebih, dan menyediakan tidak kurang."

Dalam situasi kami, satu-satunya hal yang Iterator lakukan adalah memberi kami objek yang diketik, misalnya, Hewan. Jika kita menerima Iterator di suatu tempat, kita bisa meletakkan Iterator di sana, dan mendapatkan Hewan dari metode () berikutnya, karena anjing itu juga Hewan. Kami menyediakan tidak sedikit, tetapi lebih banyak, karena seekor anjing adalah subtipe.



Saya ulangi: kami hanya membaca dari jenis ini, oleh karena itu, hubungan antara jenis dan subtipe dipertahankan di sini. Dan tipe seperti itu disebut kovarian.

Kasus lain: Aksi. Aksi adalah fungsi yang tidak mengembalikan apa-apa, mengambil satu parameter, dan kami hanya menulis ke Action, yaitu, mengambil anjing atau hewan dari kami.



Jadi, di sini kita tidak lagi menyediakan, tetapi menuntut, dan kita tidak harus menuntut lagi. Ini berarti bahwa ketergantungan kita berubah. "Tidak lebih" kami memiliki Hewan (Hewan kurang dari seekor anjing). Dan tipe seperti itu disebut contravarian.

Ada kasus ketiga - misalnya, ArrayList, dari mana kita membaca dan menulis. Karena itu, dalam hal ini, kami melanggar salah satu aturan, kami membutuhkan lebih banyak untuk catatan (anjing, bukan hewan). Jenis seperti itu tidak berhubungan dengan cara apa pun, dan mereka disebut invarian.



Jadi, di Jawa, ketika itu dirancang sebelum versi 1.5 (di mana obat generik muncul), secara default mereka membuat array kovarian. Ini berarti bahwa Anda dapat menetapkan larik string ke larik objek, lalu meneruskannya ke suatu metode di mana larik objek diperlukan, dan mencoba mendorong objek di sana, meskipun ini adalah larik string. Segalanya akan jatuh ke tangan Anda.

Setelah belajar dari pengalaman pahit bahwa hal ini tidak dapat dilakukan, ketika mendesain obat generik, mereka memutuskan "kami akan membuat koleksi tidak berubah, kami tidak akan melakukan apa pun dengan mereka."

Dan pada akhirnya ternyata bahwa dalam hal yang nampak begitu jelas semuanya harus baik-baik saja, tetapi sebenarnya tidak ok:

 // Java List<Dog> dogs = new ArrayList<>(); List<Animal> animals = dogs; 

Tetapi bagaimanapun juga kita perlu menentukan apa yang bisa kita lakukan: jika kita hanya membaca dari lembar ini, mengapa tidak memungkinkan untuk mentransfer daftar anjing di sini? Oleh karena itu, dimungkinkan untuk menandai dengan wildcard variasi apa yang akan dimiliki oleh tipe ini:

 List<Dog> dogs = new ArrayList<>(); List<? extends Animal> animals = dogs; 

Seperti yang Anda lihat, variasi ini ditunjukkan di tempat penggunaan, tempat kami menetapkan anjing. Oleh karena itu, ini disebut varians use-site.

Apa kerugiannya? Sisi negatifnya adalah Anda harus menentukan wildcard menakutkan ini di mana pun Anda menggunakan API, dan semua ini sangat bermanfaat dalam kode. Tetapi di Kotlin untuk beberapa alasan, hal seperti itu bekerja di luar kotak, dan Anda tidak perlu menentukan apa pun:

 val dogs: List<Dog> = ArrayList() val animals: List<Animal> = dogs 

Apa alasannya? Dengan fakta bahwa lembarannya sebenarnya berbeda. Daftar di Jawa berarti menulis, sedangkan di Kotlin itu hanya baca, tidak menyiratkan. Karena itu, pada prinsipnya, kita dapat langsung mengatakan bahwa kita hanya membaca dari sini, karena itu kita bisa menjadi kovarian. Dan ini diatur secara tepat dalam deklarasi tipe dengan kata kunci keluar menggantikan wildcard:

 interface List<out E> : Collection<E> 

Ini disebut varian situs deklarasi. Jadi, kami menunjukkan semuanya di satu tempat, dan di mana kami menggunakannya, kami tidak lagi menyentuh topik ini. Dan ini nishtyak.

Kembali ke kode


Mari kita kembali ke kedalaman kita. Di sini kita memiliki metode addPlugins, dibutuhkan Daftar:

 @JvmStatic fun addPlugins (plugs: List<Plugin>) { plugs.forEach { addPlugin(it) } }    ,  , List<EmptyPlugin>, ,     : <source lang="java"> final List<EmptyPlugin> pluginsToSet = Arrays.asList(new LoggerPlugin("Alog"), new SegmentPlugin()); 

Karena Daftar di Kotlin adalah kovarian, kita dapat dengan mudah melewati daftar ahli waris plugin di sini. Semuanya akan bekerja, kompiler tidak keberatan. Tetapi karena fakta bahwa kami memiliki varian situs deklarasi tempat kami menentukan semuanya, maka kami tidak dapat mengontrol koneksi dengan Java pada tahap penggunaan. Tapi apa yang terjadi jika kita benar-benar menginginkan lembar Plugin di sana, kita tidak ingin ada ahli waris di sana? Tidak ada pengubah untuk ini, tetapi apa? Itu benar, ada anotasi. Dan anotasi disebut @JvmSuppressWildcards, yaitu, secara default kami berpikir bahwa di sini adalah tipe dengan wildcard, jenisnya adalah kovarian.

 @JvmStatic fun addPlugins(plugs: List<@JvmSuppressWildcards Plugin>) { plugs.forEach { addPlugin(it) } } 

Berbicara SuppressWildcards, kami menekan semua pertanyaan ini, dan tanda tangan kami benar-benar berubah. Lebih dari itu, saya akan menunjukkan kepada Anda bagaimana segala sesuatu terlihat dalam bytecode:



Saya akan menghapus anotasi dari kode untuk saat ini. Inilah metode kami. Anda mungkin tahu bahwa penghapusan tipe ada. Dan dalam bytecode Anda tidak ada informasi tentang jenis pertanyaan apa yang ada, yah, generik secara umum. Tetapi kompilator mengikuti ini dan menandatanganinya di komentar ke bytecode: dan ini adalah tipe dengan pertanyaan.



Sekarang kita kembali memasukkan anotasi dan melihat bahwa ini adalah tipe kita tanpa bertanya.



Sekarang kode kita sebelumnya akan berhenti mengkompilasi dengan tepat karena kita memotong Wildcard. Anda bisa melihatnya sendiri.

Kami membuat jenis kovarian. .

, List . , getPlugins, . ? , , , . , Java.

 final List<Plugin> plugins = Analytics.getPlugins(); displayPlugins(plugins); Analytics.getPlugins().add(new EmptyPlugin()); 

, - , , - . , . , - .

. Kotlin , , , , , wildcards Java. , , . , List, Plugin. , , , : Plugin, .

. , , usecase, - , .

, , , - . , Java. Kotlin List — read only-, , Java — ? , List wildcard. , . @JvmWildcard : , . , Java . Java « ?»:



List<? extends Plugin>, « ?» , , . script kiddie, « , , , ArrayList, ». , ArrayList , .

 ((ArrayList<Plugin>) Analytics.getPlugins()).add(new EmptyPlugin()); 

, , , defensive-, - . , , , script kiddies .

 @JvmStatic fun getPlugins(): List<@JvmWildcard Plugin> = plugin.toImmutableList() 

, @JvmSuppressWildcard , , , , .

, . , : .

Java. , :

 @Override public void send(@NotNull Event event) throws IOException 

:

 interface Plugin { fun init() /** @throws IOException if sending failed */ fun send(event: Event) // ... } 

Kotlin checked exception. : . , , . Java -. : « Throws - , »:



-, Kotlin? , …

@Throws, . throws- . , IOExeption:

 open class EmptyPlugin : Plugin { @Throws(IOException::class) override fun send(event: Event) {} // ... } 

:

 interface Plugin { fun init() /** @throws IOException if sending failed */ @Throws(IOException::class) fun send(event: Event) // ... } 

? , Java, exception, . , . , - , , @JvmName. .

, Java . …

 package util fun List<Int>.printReversedSum() { println(this.foldRight(0) { it, acc -> it + acc }) } @JvmName("printReversedConcatenation") fun List<String>.printReversedSum() { println(this.foldRight(StringBuilder()) { it, acc -> acc.append(it) }) } 

Misalkan di Jawa kami tidak peduli di sini, hapus anotasinya. Kesalahan, sekarang IDE menunjukkan kesalahan pada kedua fungsi. Menurut Anda apa alasannya? Ya, tanpa anotasi, mereka dihasilkan dengan nama yang sama, tetapi di sini tertulis bahwa satu ada di Daftar, yang lain di Daftar. Benar, ketikkan penghapusan. Kami bahkan dapat memeriksa kasus ini:



, , top-level c. printReversedSum List, List. Kotlin- , Java- . , kotlin.jvm , Java , , Kotlin . — , concatenation — , .

. . extension- reverse.

 inline fun String.reverse() = StringBuilder(this).reverse().toString() inline fun <reified T> reversedClassName() = T::class.java.simpleName.reverse() inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element) } 

reverse , ReverserKt.

 private static void useUtils() { System.out.println(ReverserKt.reverse("Test")); SumsKt.printReversedSum(asList(1, 2, 3, 4, 5)); SumsKt.printReversedConcatenation(asList("1", "2", "3", "4", "5")); } 

, , . , , Java, - . . ? , @JvmName, , .

, , , , , .

 @file:Suppress("NOTHING_TO_INLINE") @file:JvmName("ReverserUtils") 

Java ReverserKt, , ReverserUtils . « 2.1» — , top-level , . , , sums.kt SumsKt, , reversing ReverserUtils. @JvmName, «ReverserUtils», , , , .

, , « , -». ? @JvmMultifileClass, , , .

"@file:JvmMultifileClass", SumsKt ReverserUtils, — . !

, . , , . , , , @JvmName Kotlin.

Kotlin-


, , . , Kotlin- .

, inline-. Kotlin , , Java ? , , , Java. , , Kotlin-only , dex count limit. Kotlin , .

Reified type parameters. Kotlin, - , Java . Kotlin-only , Kotlin, Java reified, .

java.lang.Class. , Java, . . « Retrofit», ( , ):

 class Retrofit private constructor( val baseUrl: String, val client: Client ) { fun <T : Any> create(service: Class<T>): T {...} fun <T : Any> create(service: KClass<T>): T { return create(service.java) } } 

, Java, , KClass, , extension-, KClass Class, Class KClass ( Kotlin, ).

, . Kotlin- KClass, Reified-, :

 inline fun <reified T : Any> create(): T { return create(T::class.java.java) 

. Kotlin , . val api = retrofit.create(Api::class) val api = retrofit.create<Api>() , ::class . Reified-, -.

Unit. Unit, , void Java, . . , . - Scala, Scala , - , , , void.

Tetapi di Kotlin ini tidak. Kotlin hanya memiliki 22 antarmuka yang menerima serangkaian parameter berbeda dan mengembalikan sesuatu. Dengan demikian, lambda yang mengembalikan Unit akan kembali bukan batal, tetapi Unit. Dan ini memberlakukan batasannya. Seperti apa lambda yang mengembalikan Unit? Sekarang, lihat dia di fragmen kode ini. Kenali satu sama lain.

 inline fun <T> Iterable<T>.forEachReversed(action: (T) -> Unit) { for (element in this.reversed()) action(element) } 

Menggunakannya dari Kotlin: semuanya baik-baik saja, kita bahkan menggunakan referensi metode, jika kita bisa, dan itu membaca dengan sempurna, mata kita tidak berperasaan.

 private fun useMisc() { listOf(1, 2, 3, 4).forEachReversed(::println) println(reversedClassName<String>()) } 

Apa yang sedang terjadi di Jawa? Di Jawa, sampan berikut ini terjadi:

 private static void useMisc() { final List<Integer> list = asList(1, 2, 3, 4); ReverserUtils.forEachReversed(list, integer -> { System.out.println(integer); return Unit.INSTANCE; }); 

- , - . Void , . , void, . , , , . , Unit . null, . , .

: Typealiases — , , Kotlin, Java, , , . , - . Java- .

: visibility. , internal visibility. , Kotlin package private, - , public. internal. Internal — , . Retrofit internal- validate.

 internal fun validate(): Retrofit { println("!!!!!! internal fun validate() was called !!!!!!") return this } 

Kotlin, . Java? validate? , , internal public. , Kotlin bytecode viewer.



public, , , , , , API . - 80 , .

Java :

 final Api api = retrofit .validate$production_sources_for_module_library_main() .create(Api.class); api.sendMessage("Hello from Java"); } 

. , , , . , let me explain this to you. , ?

 final Api api = retrofit .validate$library() .create(Api.class); api.sendMessage("Hello from Java"); } 

. « ?» … MAGIC!

, - internal, , API. script kiddie Kotlin Bytecode Viewer, . internal visibility.

, . , , , , SkillsMatter. .

, Kotlin-. , - , . Kotlin bytecode viewer .

Terima kasih

, : 8-9 Mobius , . — , .

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


All Articles