Lazy Initialization di Kotlin

1. Ikhtisar

Pada artikel ini, kita akan melihat salah satu fitur paling menarik dalam sintaks Kotlin - inisialisasi malas.

Kita juga akan melihat kata kunci lateinit yang memungkinkan kita untuk mengelabui kompiler dan menginisialisasi bidang non-null di badan kelas - bukan di konstruktor.

2. Pola Inisialisasi Malas di Java

Terkadang kita perlu membuat objek yang memiliki proses inisialisasi yang rumit. Juga, seringkali kita tidak dapat memastikan bahwa objek, yang kita bayarkan sebagai biaya inisialisasi pada awal program kita, akan digunakan dalam program kita sama sekali.

Konsep 'inisialisasi malas' dirancang untuk mencegah inisialisasi objek yang tidak perlu . Di Java, membuat objek dengan cara yang lazy dan aman untuk thread bukanlah hal yang mudah untuk dilakukan. Pola seperti Singleton memiliki kekurangan yang signifikan dalam multithreading, pengujian, dll. - dan sekarang secara luas dikenal sebagai anti-pola yang harus dihindari.

Alternatifnya, kita dapat memanfaatkan inisialisasi statis objek dalam di Java untuk mencapai kemalasan:

public class ClassWithHeavyInitialization { private ClassWithHeavyInitialization() { } private static class LazyHolder { public static final ClassWithHeavyInitialization INSTANCE = new ClassWithHeavyInitialization(); } public static ClassWithHeavyInitialization getInstance() { return LazyHolder.INSTANCE; } }

Perhatikan bagaimana, hanya ketika kita akan memanggil metode getInstance () pada ClassWithHeavyInitialization , kelas LazyHolder statis akan dimuat, dan instance baru dari ClassWithHeavyInitialization akan dibuat. Selanjutnya, instance akan ditetapkan ke referensi INSTANCE final statis .

Kita bisa menguji bahwa getInstance () mengembalikan instance yang sama setiap kali dipanggil:

@Test public void giveHeavyClass_whenInitLazy_thenShouldReturnInstanceOnFirstCall() { // when ClassWithHeavyInitialization classWithHeavyInitialization = ClassWithHeavyInitialization.getInstance(); ClassWithHeavyInitialization classWithHeavyInitialization2 = ClassWithHeavyInitialization.getInstance(); // then assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2); }

Secara teknis tidak apa-apa, tetapi tentu saja agak terlalu rumit untuk konsep sederhana seperti itu .

3. Inisialisasi Malas di Kotlin

Kita bisa melihat bahwa menggunakan pola inisialisasi malas di Java cukup merepotkan. Kita perlu menulis banyak kode boilerplate untuk mencapai tujuan kita. Untungnya, bahasa Kotlin memiliki dukungan bawaan untuk inisialisasi lambat .

Untuk membuat objek yang akan diinisialisasi saat pertama kali mengaksesnya, kita dapat menggunakan metode lazy :

@Test fun givenLazyValue_whenGetIt_thenShouldInitializeItOnlyOnce() { // given val numberOfInitializations: AtomicInteger = AtomicInteger() val lazyValue: ClassWithHeavyInitialization by lazy { numberOfInitializations.incrementAndGet() ClassWithHeavyInitialization() } // when println(lazyValue) println(lazyValue) // then assertEquals(numberOfInitializations.get(), 1) }

Seperti yang bisa kita lihat, lambda yang diteruskan ke fungsi lazy dijalankan hanya sekali.

Saat kita mengakses lazyValue untuk pertama kalinya - inisialisasi aktual terjadi, dan instance kelas ClassWithHeavyInitialization yang dikembalikan ditetapkan ke referensi lazyValue . Akses selanjutnya ke lazyValue mengembalikan objek yang diinisialisasi sebelumnya.

Kita bisa meneruskan LazyThreadSafetyMode sebagai argumen ke fungsi lazy . Mode publikasi default adalah SYNCHRONIZED , yang berarti bahwa hanya satu thread yang dapat menginisialisasi objek yang diberikan.

Kita dapat mengirimkan PUBLIKASI sebagai mode - yang akan menyebabkan setiap utas dapat menginisialisasi properti yang diberikan. Objek yang ditetapkan ke referensi akan menjadi nilai yang dikembalikan pertama - jadi utas pertama menang.

Mari kita lihat skenario itu:

@Test fun whenGetItUsingPublication_thenCouldInitializeItMoreThanOnce() { // given val numberOfInitializations: AtomicInteger = AtomicInteger() val lazyValue: ClassWithHeavyInitialization by lazy(LazyThreadSafetyMode.PUBLICATION) { numberOfInitializations.incrementAndGet() ClassWithHeavyInitialization() } val executorService = Executors.newFixedThreadPool(2) val countDownLatch = CountDownLatch(1) // when executorService.submit { countDownLatch.await(); println(lazyValue) } executorService.submit { countDownLatch.await(); println(lazyValue) } countDownLatch.countDown() // then executorService.awaitTermination(1, TimeUnit.SECONDS) executorService.shutdown() assertEquals(numberOfInitializations.get(), 2) }

Kita dapat melihat bahwa memulai dua utas pada saat yang sama menyebabkan inisialisasi ClassWithHeavyInitialization terjadi dua kali.

Ada juga mode ketiga - TIDAK ADA - tetapi tidak boleh digunakan di lingkungan multithread karena perilakunya tidak ditentukan.

4. Ketidaktahuan Kotlin

Di Kotlin, setiap properti kelas non-nullable yang dideklarasikan di kelas harus diinisialisasi di konstruktor atau sebagai bagian dari deklarasi variabel. Jika kita gagal melakukannya, maka compiler Kotlin akan mengeluh dengan pesan error:

Kotlin: Property must be initialized or be abstract

Ini pada dasarnya berarti bahwa kita harus menginisialisasi variabel atau menandainya sebagai abstrak .

Di sisi lain, ada beberapa kasus di mana variabel dapat ditetapkan secara dinamis dengan, misalnya injeksi ketergantungan.

Untuk menunda inisialisasi variabel, kita dapat menentukan bahwa sebuah field adalah lateinit . Kami memberi tahu compiler bahwa variabel ini akan ditetapkan nanti dan kami membebaskan compiler dari tanggung jawab untuk memastikan bahwa variabel ini diinisialisasi:

lateinit var a: String @Test fun givenLateInitProperty_whenAccessItAfterInit_thenPass() { // when a = "it" println(a) // then not throw }

Jika kita lupa menginisialisasi properti lateinit , kita akan mendapatkan UninitializedPropertyAccessException :

@Test(expected = UninitializedPropertyAccessException::class) fun givenLateInitProperty_whenAccessItWithoutInit_thenThrow() { // when println(a) }

Perlu disebutkan bahwa kita hanya dapat menggunakan variabel lateinit dengan tipe data non-primitif. Oleh karena itu, tidak mungkin untuk menulis sesuatu seperti ini:

lateinit var value: Int

Dan jika kita melakukannya, kita akan mendapatkan kesalahan kompilasi:

Kotlin: 'lateinit' modifier is not allowed on properties of primitive types

5. Kesimpulan

Dalam tutorial singkat ini, kita melihat inisialisasi objek yang lambat.

Pertama, kami melihat cara membuat inisialisasi malas yang aman untuk thread di Java. Kami melihat bahwa ini sangat rumit dan membutuhkan banyak kode boilerplate.

Selanjutnya, kita mempelajari kata kunci malas Kotlin yang digunakan untuk inisialisasi properti yang lambat. Pada akhirnya, kami melihat bagaimana cara menunda penugasan variabel menggunakan kata kunci lateinit .

Penerapan semua contoh dan cuplikan kode ini dapat ditemukan di GitHub.