Lazy Initialization i Kotlin

1. Oversigt

I denne artikel ser vi på en af ​​de mest interessante funktioner i Kotlin-syntaks - doven initialisering.

Vi vil også se på lateinit nøgleord, der giver os mulighed for at narre kompilatoren og initialisere ikke-nul felter i klassen - i stedet for i konstruktøren.

2. Lazy Initialization Pattern i Java

Nogle gange er vi nødt til at konstruere objekter, der har en besværlig initialiseringsproces. Ofte kan vi heller ikke være sikre på, at det objekt, som vi betalte initialiseringsomkostningerne for i starten af ​​vores program, overhovedet vil blive brugt i vores program.

Begrebet 'Doven initialisering' var designet til at forhindre unødvendig initialisering af objekter. I Java er det ikke let at oprette et objekt på en doven og trådsikker måde. Mønstre som Singleton har betydelige mangler ved multitrådning, test osv. - og de er nu almindeligt kendt som antimønstre, der skal undgås.

Alternativt kan vi udnytte den statiske initialisering af det indre objekt i Java for at opnå dovenskab:

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

Læg mærke til hvordan, kun når vi kalder getInstance () metode til ClassWithHeavyInitialization, det statiske LazyHolder klasse indlæses, og den nye forekomst af ClassWithHeavyInitialization vil blive oprettet. Derefter tildeles forekomsten til statiskendeligINSTANS reference.

Vi kan teste, at getInstance () returnerer den samme forekomst hver gang det kaldes:

@Test offentlig ugyldighed giveHeavyClass_whenInitLazy_thenShouldReturnInstanceOnFirstCall () {// når ClassWithHeavyInitialization classWithHeavyInitialization = ClassWithHeavyInitialization.getInstance (); ClassWithHeavyInitialization classWithHeavyInitialization2 = ClassWithHeavyInitialization.getInstance (); // derefter assertTrue (classWithHeavyInitialization == classWithHeavyInitialization2); }

Det er teknisk OK, men selvfølgelig lidt for kompliceret til et så simpelt koncept.

3. Lazy initialisering i Kotlin

Vi kan se, at brug af det dovne initialiseringsmønster i Java er ret besværligt. Vi er nødt til at skrive en masse kedelpladekode for at nå vores mål. Heldigvis har Kotlin-sproget indbygget support til doven initialisering.

For at oprette et objekt, der initialiseres ved første adgang til det, kan vi bruge doven metode:

@Test fun givenLazyValue_whenGetIt_thenShouldInitializeItOnlyOnce () {// given val numberOfInitializations: AtomicInteger = AtomicInteger () val lazyValue: ClassWithHeavyInitialization by lazy {numberOfInitializations.incrementAnd numberOfInitializations.get (), 1)}

Som vi kan se, gik lambda til doven funktionen blev kun udført en gang.

Når vi får adgang til lazyValue for første gang - der skete en faktisk initialisering, og den returnerede forekomst af ClassWithHeavyInitialization klasse blev tildelt lazyValue reference. Efterfølgende adgang til lazyValue returnerede det tidligere initialiserede objekt.

Vi kan passere LazyThreadSafetyMode som et argument til doven fungere. Standardpublikationstilstanden er SYNKRONISERET, hvilket betyder, at kun en enkelt tråd kan initialisere det givne objekt.

Vi kan passere en OFFENTLIGGØRELSE som en tilstand - hvilket vil medføre, at hver tråd kan initialisere den givne egenskab. Objektet, der tildeles referencen, er den første returnerede værdi - så den første tråd vinder.

Lad os se på dette scenario:

@Test fun whenGetItUsingPublication_thenCouldInitializeItMoreThanOnce () {// givet val numberOfInitializations: AtomicInteger = AtomicInteger () val lazyValue: ClassWithHeavyInitialization ved dovne (LazyThreadSafetyMode.PUBLICATION) {numberOfInitializations.incrementAndGet () ClassWithHeavyInitialization ()} val ExecutorService = Executors.newFixedThreadPool (2) val countDownLatch = CountDownLatch (1) // når executorService.submit {countDownLatch.await (); println (lazyValue)} executorService.submit {countDownLatch.await (); println (lazyValue)} countDownLatch.countDown () // derefter executorService.awaitTermination (1, TimeUnit.SECONDS) executorService.shutdown () assertEquals (numberOfInitializations.get (), 2)}

Vi kan se, at start af to tråde på samme tid forårsager initialiseringen af ClassWithHeavyInitialization at ske to gange.

Der er også en tredje tilstand - INGEN - men det bør ikke bruges i det multitrådede miljø, da dets adfærd er udefineret.

4. Kotlins lateinit

I Kotlin skal enhver ikke-ugyldig klasseegenskab, der er deklareret i klassen, initialiseres enten i konstruktøren eller som en del af variabeldeklarationen. Hvis vi ikke gør det, klager Kotlin-kompilatoren med en fejlmeddelelse:

Kotlin: Ejendom skal initialiseres eller være abstrakt

Dette betyder grundlæggende, at vi enten skal initialisere variablen eller markere den som abstrakt.

På den anden side er der nogle tilfælde, hvor variablen kan tildeles dynamisk ved f.eks. Afhængighedsinjektion.

For at udskyde initialiseringen af ​​variablen kan vi specificere, at et felt er lateinit. Vi informerer compileren om, at denne variabel vil blive tildelt senere, og vi frigør compileren fra ansvaret for at sikre, at denne variabel bliver initialiseret:

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

Hvis vi glemmer at initialisere lateinit ejendom, vi får en UninitializedPropertyAccessException:

@Test (forventet = UninitializedPropertyAccessException :: klasse) sjov givenLateInitProperty_whenAccessItWithoutInit_thenThrow () {// når println (a)}

Det er værd at nævne, at vi kun kan bruge lateinit variabler med ikke-primitive datatyper. Derfor er det ikke muligt at skrive noget som dette:

lateinit var værdi: Int

Og hvis vi gør det, får vi en kompileringsfejl:

Kotlin: 'lateinit' modifikator er ikke tilladt på grund af primitive typer egenskaber

5. Konklusion

I denne hurtige vejledning så vi på den dovne initialisering af objekter.

For det første så vi, hvordan man opretter en trådsikker doven initialisering i Java. Vi så, at det er meget besværligt og har brug for en masse kedelpladekode.

Derefter dykkede vi ind i Kotlin doven nøgleord, der bruges til doven initialisering af egenskaber. I sidste ende så vi, hvordan man udsætter tildeling af variabler ved hjælp af lateinit nøgleord.

Implementeringen af ​​alle disse eksempler og kodestykker findes på GitHub.