
Kembali pada Mei 2017, Google mengumumkan bahwa Kotlin telah menjadi bahasa pengembangan resmi untuk Android. Seseorang kemudian mendengar nama bahasa ini untuk pertama kalinya, seseorang menulisnya untuk waktu yang lama, tetapi sejak saat itu menjadi jelas bahwa setiap orang yang dekat dengan pengembangan Android sekarang berkewajiban untuk mengetahuinya. Ini diikuti oleh tanggapan antusias "Akhirnya!" Dan kemarahan yang mengerikan "Mengapa kita membutuhkan bahasa baru?" Apa yang tidak disukai Java? " dll. dll.
Sudah cukup waktu berlalu sejak itu, dan meskipun perdebatan tentang apakah Kotlin baik atau buruk masih belum surut, semakin banyak kode untuk Android yang tertulis di sana. Dan bahkan pengembang yang cukup konservatif juga beralih ke sana. Selain itu, pada jaringan Anda dapat menemukan informasi bahwa kecepatan pengembangan setelah menguasai bahasa ini meningkat 30% dibandingkan dengan Jawa.
Hari ini, Kotlin telah berhasil pulih dari beberapa penyakit masa kanak-kanak, ditumbuhi banyak pertanyaan dan jawaban tentang Stack Overflow. Dengan mata telanjang, baik kelebihan maupun kelemahannya menjadi terlihat.
Dan pada gelombang ini, saya terpikir untuk menganalisis secara rinci elemen-elemen individual dari bahasa yang muda namun populer. Perhatikan poin-poin kompleks dan bandingkan dengan Java untuk kejelasan dan pemahaman yang lebih baik. Untuk memahami pertanyaan sedikit lebih dalam dari ini bisa dilakukan dengan membaca dokumentasi. Jika artikel ini membangkitkan minat, maka kemungkinan besar itu akan meletakkan dasar untuk seluruh rangkaian artikel. Sementara itu, saya akan mulai dengan hal-hal yang cukup mendasar, yang, bagaimanapun, menyembunyikan banyak jebakan. Mari kita bicara tentang konstruktor dan inisialisasi di Kotlin.
Seperti di Jawa, di Kotlin, penciptaan objek baru - entitas dari tipe tertentu - terjadi dengan memanggil konstruktor kelas. Anda juga bisa meneruskan argumen ke konstruktor, dan mungkin ada beberapa konstruktor. Jika Anda melihat proses ini dari luar, maka satu-satunya perbedaan dari Jawa adalah kurangnya kata kunci baru saat memanggil konstruktor. Sekarang perhatikan lebih dalam dan lihat apa yang terjadi di dalam kelas.
Kelas dapat memiliki konstruktor primer dan sekunder.
Konstruktor dinyatakan menggunakan kata kunci konstruktor. Jika konstruktor utama tidak memiliki pengubah akses dan anotasi, kata kunci dapat dihilangkan.
Kelas mungkin tidak memiliki konstruktor yang dinyatakan secara eksplisit. Dalam hal ini, setelah deklarasi kelas tidak ada konstruksi, kami segera melanjutkan ke tubuh kelas. Jika kita menggambar analogi dengan Java, ini setara dengan tidak adanya deklarasi konstruktor yang eksplisit, sebagai akibatnya konstruktor default (tanpa parameter) akan dihasilkan secara otomatis pada tahap kompilasi. Tampak seperti yang diharapkan:
class MyClassA
Ini sama dengan entri berikut:
class MyClassA constructor()
Tetapi jika Anda menulis dengan cara ini, maka Anda akan dengan sopan diminta untuk menghapus konstruktor utama tanpa parameter.
Konstruktor utama adalah salah satu yang selalu dipanggil ketika suatu objek dibuat jika ada. Sementara kami mempertimbangkan ini, dan kami akan menganalisisnya secara lebih rinci nanti, ketika kami beralih ke konstruktor sekunder. Dengan demikian, kita ingat bahwa jika tidak ada konstruktor sama sekali, maka sebenarnya ada satu (primer) satu, tetapi kita tidak melihatnya.
Jika, misalnya, kami ingin konstruktor utama tanpa parameter tidak memiliki akses publik, maka bersama dengan modifikasi
private
kita perlu mendeklarasikannya secara eksplisit dengan kata kunci
constructor
.
Fitur utama dari konstruktor utama adalah bahwa ia tidak memiliki tubuh, mis. tidak dapat berisi kode yang dapat dieksekusi. Ini hanya mengambil parameter ke dalam dirinya sendiri dan meneruskannya jauh ke kelas untuk digunakan di masa depan. Pada level sintaks, tampilannya seperti ini:
class MyClassA constructor(param1: String, param2: Int, param3: Boolean)
Parameter yang diteruskan dengan cara ini dapat digunakan untuk berbagai inisialisasi, tetapi tidak lebih. Dalam bentuknya yang murni, kita tidak bisa menggunakan argumen ini dalam kode kerja kelas. Namun, kita dapat menginisialisasi bidang kelas di sini. Ini terlihat seperti ini:
class MyClassA constructor(val param1: String, var param2: Int, param3: Boolean)
Di sini,
param1
dan
param2
dapat digunakan dalam kode sebagai bidang kelas, yang setara dengan yang berikut:
class MyClassA constructor(p1: String, p2: Int, param3: Boolean)
Nah, jika Anda membandingkan dengan Java, maka akan terlihat seperti ini (dan omong-omong, dalam contoh ini Anda dapat mengevaluasi seberapa banyak Kotlin dapat mengurangi jumlah kode):
public class MyClassAJava { private final String param1; private Integer param2; public MyClassAJava(String p1, Integer p2, Boolean param3) { this.param1 = p1; this.param2 = p2; } public String getParam1() { return param1; } public Integer getParam2() { return param2; } public void setParam2(final Integer param2) { this.param2 = param2; }
Mari kita bicara tentang desainer tambahan. Mereka lebih mengingatkan pada konstruktor biasa di Jawa: mereka menerima parameter, dan mungkin memiliki blok yang dapat dieksekusi. Saat mendeklarasikan konstruktor tambahan, kata kunci konstruktor diperlukan. Seperti disebutkan sebelumnya, terlepas dari kemungkinan membuat objek dengan memanggil konstruktor tambahan, konstruktor utama (jika ada) juga harus dipanggil dengan bantuan
this
. Pada level sintaks, ini disusun sebagai berikut:
class MyClassA(val p1: String) { constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
Yaitu konstruktor tambahan adalah, seolah-olah, pewaris utama.
Sekarang, jika kita membuat objek dengan memanggil konstruktor tambahan, hal berikut akan terjadi:
memanggil konstruktor tambahan;
memanggil konstruktor utama;
inisialisasi bidang
p1
kelas di konstruktor utama;
eksekusi kode di tubuh konstruktor tambahan.
Ini mirip dengan konstruksi di Jawa:
class MyClassAJava { private final String param1; public MyClassAJava(String p1) { param1 = p1; } public MyClassAJava(String p1, Integer p2, Boolean param3) { this(p1);
Ingatlah bahwa di Jawa kita dapat memanggil satu konstruktor dari yang lain menggunakan
this
hanya di awal badan konstruktor. Di Kotlin, masalah ini secara mendasar diselesaikan - mereka menjadikan panggilan semacam itu sebagai bagian dari tanda tangan konstruktor. Untuk jaga-jaga, saya perhatikan bahwa dilarang memanggil konstruktor (primer atau tambahan) langsung dari badan yang tambahan.
Konstruktor tambahan harus selalu merujuk ke konstruktor utama (jika ada), tetapi dapat melakukannya secara tidak langsung, merujuk pada konstruktor tambahan lainnya. Intinya adalah bahwa pada akhir rantai kita masih sampai pada hal utama. Pemicu konstruktor jelas akan terjadi dalam urutan terbalik dari perancang yang berpaling satu sama lain:
class MyClassA(p1: String) constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3)
Sekarang urutannya adalah:
- memanggil konstruktor tambahan dengan 4 parameter;
- memanggil konstruktor tambahan dengan 3 parameter;
- memanggil konstruktor utama;
- inisialisasi bidang p1 kelas di konstruktor utama;
- eksekusi kode di tubuh konstruktor dengan 3 parameter;
- eksekusi kode dalam tubuh konstruktor dengan 4 parameter.
Bagaimanapun, kompiler tidak akan pernah membiarkan kita lupa untuk sampai ke konstruktor utama.
Itu terjadi bahwa kelas tidak memiliki konstruktor utama, sementara itu mungkin memiliki satu atau lebih tambahan. Kemudian konstruktor tambahan tidak diperlukan untuk merujuk ke seseorang, tetapi mereka juga dapat merujuk ke konstruktor tambahan lain dari kelas ini. Sebelumnya, kami menemukan bahwa konstruktor utama, tidak ditentukan secara eksplisit, secara otomatis dihasilkan, tetapi ini berlaku untuk kasus ketika tidak ada konstruktor sama sekali di dalam kelas. Jika ada setidaknya satu konstruktor tambahan, konstruktor utama tanpa parameter tidak dibuat:
class MyClassA {
Kita dapat membuat objek kelas dengan memanggil:
val myClassA = MyClassA()
Dalam hal ini:
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) {
Kami hanya dapat membuat objek dengan panggilan ini:
val myClassA = MyClassA(“some string”, 10, True)
Tidak ada yang baru di Kotlin dibandingkan dengan Jawa.
Omong-omong, seperti konstruktor utama, konstruktor tambahan mungkin tidak memiliki badan jika tugasnya hanya meneruskan parameter ke konstruktor lain.
class MyClassA { constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "") constructor(p1: String, p2: Int, p3: Boolean, p4: String) {
Penting juga memperhatikan fakta bahwa, tidak seperti konstruktor utama, inisialisasi bidang kelas dalam daftar argumen konstruktor tambahan dilarang.
Yaitu catatan seperti itu tidak valid:
class MyClassA { constructor(val p1: String, var p2: Int, p3: Boolean){
Secara terpisah, perlu dicatat bahwa konstruktor tambahan, seperti yang primer, mungkin tanpa parameter:
class MyClassA { constructor(){
Berbicara tentang konstruktor, orang tidak bisa tidak menyebutkan salah satu fitur nyaman Kotlin - kemampuan untuk menetapkan nilai default untuk argumen.
Sekarang anggaplah kita memiliki kelas dengan beberapa konstruktor yang memiliki jumlah argumen yang berbeda. Saya akan memberikan contoh di Jawa:
public class MyClassAJava { private String param1; private Integer param2; private boolean param3; private int param4; public MyClassAJava(String p1) { this (p1, 5); } public MyClassAJava(String p1, Integer p2) { this (p1, p2, true); } public MyClassAJava(String p1, Integer p2, boolean p3) { this(p1, p2, p3, 20); } public MyClassAJava(String p1, Integer p2, boolean p3, int p4) { this.param1 = p1; this.param2 = p2; this.param3 = p3; this.param4 = p4; }
Seperti yang ditunjukkan dalam praktik, desain semacam itu sangat umum. Mari kita lihat bagaimana hal yang sama dapat ditulis di Kotlin:
class MyClassA (var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
Sekarang, mari kita menepuk-nepuk Kotlin untuk berapa banyak dia memotong kode. Omong-omong, selain mengurangi jumlah garis, kami mendapat lebih banyak pesanan. Ingat, Anda harus melihat sesuatu seperti ini lebih dari sekali:
public MyClassAJava(String p1, Integer p2, boolean p3) { this(p3, p1, p2, 20); } public MyClassAJava(boolean p1, String p2, Integer p3, int p4) {
Ketika Anda melihat ini, Anda ingin menemukan orang yang menulisnya, mengambilnya dengan tombol, membawanya ke layar dan bertanya dengan suara sedih: "Mengapa?"
Meskipun Anda dapat mengulangi prestasi ini di Kotlin, tetapi tidak perlu.
Namun, ada satu detail yang, dalam kasus notasi singkat tentang Kotlin, perlu dipertimbangkan: jika kita ingin memanggil konstruktor dengan nilai default dari Java, maka kita harus menambahkan anotasi
@JvmOverloads
ke dalamnya:
class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20)
Kalau tidak, kami mendapatkan kesalahan.
Sekarang mari kita bicara
tentang inisialisasi .
Inisialisasi adalah blok kode yang ditandai dengan kata kunci
init
. Di blok ini, Anda bisa melakukan beberapa logika untuk menginisialisasi elemen-elemen kelas, termasuk menggunakan nilai-nilai argumen yang datang di konstruktor utama. Kami juga dapat memanggil fungsi dari blok ini.
Java juga memiliki blok inisialisasi, tetapi ini bukan hal yang sama. Di dalamnya, kita tidak bisa, seperti di Kotlin, memberikan nilai dari luar (argumen konstruktor utama). Penginisialisasi sangat mirip dengan badan konstruktor utama, diambil dalam blok terpisah. Tapi itu pada pandangan pertama. Sebenarnya, ini tidak sepenuhnya benar. Mari kita perbaiki.
Inisialisasi juga dapat ada ketika tidak ada konstruktor utama. Jika demikian, maka kodenya, seperti semua proses inisialisasi, dijalankan sebelum kode konstruktor tambahan. Mungkin ada lebih dari satu penginisialisasi. Dalam hal ini, urutan panggilan mereka akan sesuai dengan urutan lokasi mereka dalam kode. Perhatikan juga bahwa inisialisasi bidang kelas dapat terjadi di luar blok
init
. Dalam hal ini, inisialisasi juga terjadi sesuai dengan susunan elemen dalam kode, dan ini harus diperhitungkan ketika memanggil metode dari blok initializer. Jika Anda menerimanya dengan sembarangan, maka ada kemungkinan terjadi kesalahan.
Saya akan memberi Anda beberapa kasus menarik bekerja dengan inisialisasi.
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } var testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
Kode ini cukup valid, meskipun tidak terlalu jelas. Jika Anda melihat, Anda bisa melihat bahwa penetapan nilai ke bidang
testParam
di blok initializer terjadi sebelum parameter dideklarasikan. Omong-omong, ini hanya berfungsi jika kita memiliki konstruktor tambahan di kelas, tetapi tidak memiliki yang utama (jika kita menaikkan deklarasi bidang
testParam
atas blok
init
, ia akan bekerja tanpa konstruktor). Jika kami mendekompilasi kode byte kelas ini di Java, kami mendapatkan yang berikut:
public class MyClassB { @NotNull private String testParam = "some string"; @NotNull public final String getTestParam() { return this.testParam; } public final void setTestParam(@NotNull String var1) { Intrinsics.checkParameterIsNotNull(var1, "<set-?>"); this.testParam = var1; } public final void showTestParam() { Log.i("wow", "in showTestParam testParam = " + this.testParam); } public MyClassB() { this.showTestParam(); this.testParam = "new string"; this.testParam = "after"; Log.i("wow", "in constructor testParam = " + this.testParam); } }
Di sini kita melihat bahwa panggilan pertama ke lapangan selama inisialisasi (di blok
init
atau di luarnya) setara dengan inisialisasi biasa di Jawa. Semua tindakan lain yang terkait dengan penugasan nilai selama proses inisialisasi, kecuali untuk yang pertama (penugasan pertama nilai dikombinasikan dengan deklarasi lapangan), ditransfer ke konstruktor.
Jika kita melakukan percobaan dengan dekompilasi, ternyata jika tidak ada konstruktor, maka konstruktor utama dihasilkan, dan semua keajaiban terjadi di dalamnya. Jika ada beberapa konstruktor tambahan yang tidak saling merujuk, dan tidak ada yang utama, maka dalam kode Java kelas ini semua penugasan selanjutnya dari nilai ke bidang
testParam
diduplikasi di semua konstruktor tambahan. Jika ada konstruktor utama, maka hanya di primer. Fuf ...
Dan hal yang paling menarik sebagai
testParam
:
testParam
ubah tanda tangan
testParam
dari
var
ke
val
:
class MyClassB { init { testParam = "some string" showTestParam() } init { testParam = "new string" } val testParam: String = "after" constructor(){ Log.i("wow", "in constructor testParam = $testParam") } fun showTestParam(){ Log.i("wow", "in showTestParam testParam = $testParam") } }
Dan di suatu tempat dalam kode yang kita sebut:
MyClassB myClassB = new MyClassB();
Semuanya dikompilasi tanpa kesalahan, dimulai, dan sekarang kita melihat output dari log:
di showTestParam testParam = beberapa string
dalam konstruktor testParam = setelah
Ternyata bidang yang dinyatakan sebagai
val
mengubah nilai selama eksekusi kode. Kenapa begitu Saya pikir ini adalah cacat pada kompiler Kotlin, dan di masa depan, mungkin ini tidak akan dikompilasi, tetapi hari ini semuanya seperti itu.
Menarik kesimpulan dari kasus-kasus di atas, seseorang hanya dapat menyarankan untuk tidak menghasilkan blok inisialisasi dan tidak menyebarkan mereka di seluruh kelas, untuk menghindari penugasan nilai yang berulang selama proses inisialisasi, untuk memanggil hanya fungsi murni dari blok init. Semua ini dilakukan untuk menghindari kemungkinan kebingungan.
Jadi
Inisialisasi adalah blok kode tertentu yang harus dijalankan ketika membuat objek, terlepas dari konstruktor mana objek ini dibuat.Tampaknya beres. Pertimbangkan interaksi konstruktor dan inisialisasi. Dalam satu kelas, semuanya cukup sederhana, tetapi Anda harus ingat:
- memanggil konstruktor tambahan;
- memanggil konstruktor utama;
- inisialisasi bidang kelas dan blok penginisialisasi dalam urutan lokasi mereka dalam kode;
- eksekusi kode di tubuh konstruktor tambahan.
Kasing dengan pewarisan terlihat lebih menarik.
Perlu dicatat bahwa Object adalah basis untuk semua kelas di Jawa, jadi Any ada di Kotlin. Namun, Any dan Object bukan hal yang sama.
Untuk memulai tentang cara kerja warisan. Kelas turunan, seperti kelas induk, mungkin atau mungkin tidak memiliki konstruktor utama, tetapi harus merujuk ke konstruktor spesifik dari kelas induk.
Jika kelas turunan memiliki konstruktor utama, maka konstruktor ini harus menunjuk ke konstruktor spesifik dari kelas dasar. Dalam hal ini, semua konstruktor tambahan dari kelas penerus harus merujuk ke konstruktor utama kelas mereka.
class MyClassC(p1: String): MyClassA(p1) { constructor(p1: String, p2: Int): this(p1) { //some code } //some code }
Jika kelas turunan tidak memiliki konstruktor utama, masing-masing konstruktor tambahan harus mengakses konstruktor dari kelas induk menggunakan kata kunci
super
. Dalam hal ini, konstruktor tambahan yang berbeda dari kelas penerus dapat mengakses konstruktor yang berbeda dari kelas induk:
class MyClassC : MyClassA constructor(p1: String, p2: Int): super(p1, p2)
Juga, jangan lupa tentang kemungkinan memanggil konstruktor kelas induk secara tidak langsung melalui konstruktor lain dari kelas turunan:
class MyClassC : MyClassA constructor(p1: String, p2: Int): this (p1)
Jika kelas turunan tidak memiliki konstruktor, maka kita cukup menambahkan panggilan konstruktor dari kelas induk setelah nama kelas turunan:
class MyClassC: MyClassA(“some string”) {
Namun, masih ada opsi dengan pewarisan, di mana referensi ke konstruktor dari kelas induk tidak diperlukan. Catatan seperti itu valid:
class MyClassC : MyClassB constructor(p1: String)
Tetapi hanya jika kelas induk memiliki konstruktor tanpa parameter, yang merupakan konstruktor default (primer atau opsional - tidak masalah).
Sekarang pertimbangkan urutan permohonan inisialisasi dan konstruktor selama pewarisan:
- panggil konstruktor tambahan ahli waris;
- sebut konstruktor utama pewaris;
- memanggil konstruktor tambahan dari induk;
- hubungi konstruktor utama orang tua;
init
blok init
induk- pelaksanaan kode tubuh dari konstruktor tambahan dari induk;
- eksekusi blok ahli waris;
- pelaksanaan kode tubuh dari konstruktor tambahan ahli waris
Mari kita bicara tentang perbandingan dengan Java, di mana, pada kenyataannya, tidak ada analog dari konstruktor utama dari Kotlin. Di Jawa, semua konstruktor adalah rekan dan dapat dipanggil atau tidak dipanggil dari satu sama lain. Di Jawa dan Kotlin ada konstruktor default, itu adalah konstruktor tanpa parameter, tetapi memperoleh status khusus hanya ketika mewarisi. Di sini perlu diperhatikan hal-hal berikut: ketika mewarisi di Kotlin, kita harus secara eksplisit memberi tahu kelas penerus yang mana konstruktor dari kelas induk untuk digunakan - kompilator tidak akan membiarkan kita melupakannya. Di Jawa, kami tidak dapat menunjukkan ini secara eksplisit. Hati-hati: dalam hal ini, konstruktor default dari kelas induk akan dipanggil (jika ada).
Pada tahap ini, kita akan mengasumsikan bahwa kita mempelajari desainer dan inisialisasi dengan cukup dalam dan sekarang kita tahu hampir semua tentang mereka. Kami akan beristirahat sedikit dan menggali ke arah lain!