1. Översikt
I den här artikeln tittar vi på en av de mest intressanta funktionerna i Kotlin-syntax - lat initialisering.
Vi tittar också på nyckelordet lateinit som gör att vi kan lura kompilatorn och initiera icke-nollfält i klassen - istället för i konstruktören.
2. Lazy Initialization Pattern i Java
Ibland behöver vi konstruera objekt som har en besvärlig initialiseringsprocess. Ofta kan vi inte vara säkra på att objektet, för vilket vi betalade initialiseringskostnaden för i början av vårt program, alls kommer att användas i vårt program.
Begreppet "lat initialisering" utformades för att förhindra onödig initialisering av objekt . I Java är det inte lätt att skapa ett objekt på ett lat och trådsäkert sätt. Mönster som Singleton har betydande brister i flertrådning, testning etc. - och de är nu allmänt kända som antimönster som ska undvikas.
Alternativt kan vi använda den statiska initialiseringen av det inre objektet i Java för att uppnå latskap:
public class ClassWithHeavyInitialization { private ClassWithHeavyInitialization() { } private static class LazyHolder { public static final ClassWithHeavyInitialization INSTANCE = new ClassWithHeavyInitialization(); } public static ClassWithHeavyInitialization getInstance() { return LazyHolder.INSTANCE; } }
Lägg märke till hur, endast när vi kommer att anropa getInstance () -metoden på ClassWithHeavyInitialization , laddas den statiska LazyHolder- klassen och den nya instansen av ClassWithHeavyInitialization skapas. Därefter tilldelas instansen till den statiska slutliga INSTANCE- referensen.
Vi kan testa att getInstance () returnerar samma instans varje gång det kallas:
@Test public void giveHeavyClass_whenInitLazy_thenShouldReturnInstanceOnFirstCall() { // when ClassWithHeavyInitialization classWithHeavyInitialization = ClassWithHeavyInitialization.getInstance(); ClassWithHeavyInitialization classWithHeavyInitialization2 = ClassWithHeavyInitialization.getInstance(); // then assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2); }
Det är tekniskt OK men naturligtvis lite för komplicerat för ett så enkelt koncept .
3. Lata initialisering i Kotlin
Vi kan se att det är ganska besvärligt att använda det lata initialiseringsmönstret i Java. Vi måste skriva en hel del pannkod för att uppnå vårt mål. Lyckligtvis har Kotlin-språket inbyggt stöd för lat initialisering .
För att skapa ett objekt som kommer att initialiseras vid första åtkomst till det kan vi använda den lata metoden:
@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) }
Som vi kan se, kördes lambda till den lata funktionen bara en gång.
När vi öppnar lazyValue för första gången - en faktisk initialisering hände och den returnerade instansen av ClassWithHeavyInitialization- klassen tilldelades referensen lazyValue . Efterföljande åtkomst till lazyValue returnerade det tidigare initialiserade objektet.
Vi kan skicka LazyThreadSafetyMode som ett argument till den lata funktionen. Standardpublikationsläget är SYNKRONISERAT , vilket innebär att endast en enda tråd kan initiera det givna objektet.
Vi kan skicka en PUBLIKATION som ett läge - vilket kommer att leda till att varje tråd kan initiera en given egenskap. Objektet som tilldelas referensen är det första returnerade värdet - så den första tråden vinner.
Låt oss ta en titt på det scenariot:
@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) }
Vi kan se att start av två trådar samtidigt gör att initieringen av ClassWithHeavyInitialization sker två gånger.
Det finns också ett tredje läge - INGEN - men det ska inte användas i den multitrådade miljön eftersom dess beteende är odefinierat.
4. Kotlins lateinit
I Kotlin bör varje icke-ogiltig klassegenskap som deklareras i klassen initieras antingen i konstruktören eller som en del av variabeldeklarationen. Om vi inte gör det kommer Kotlin-kompilatorn att klaga med ett felmeddelande:
Kotlin: Property must be initialized or be abstract
Detta innebär i princip att vi antingen bör initialisera variabeln eller markera den som abstrakt .
Å andra sidan finns det några fall där variabeln kan tilldelas dynamiskt genom till exempel beroendeinjektion.
För att skjuta upp initialiseringen av variabeln kan vi ange att ett fält är seninit . Vi informerar kompilatorn om att den här variabeln kommer att tilldelas senare och vi frigör kompilatorn från ansvaret för att se till att denna variabel initialiseras:
lateinit var a: String @Test fun givenLateInitProperty_whenAccessItAfterInit_thenPass() { // when a = "it" println(a) // then not throw }
Om vi glömmer att initiera egenskapen lateinit får vi ett UninitializedPropertyAccessException :
@Test(expected = UninitializedPropertyAccessException::class) fun givenLateInitProperty_whenAccessItWithoutInit_thenThrow() { // when println(a) }
Det är värt att nämna att vi endast kan använda lateinit- variabler med icke-primitiva datatyper. Därför är det inte möjligt att skriva något så här:
lateinit var value: Int
Och om vi gör det skulle vi få ett kompileringsfel:
Kotlin: 'lateinit' modifier is not allowed on properties of primitive types
5. Sammanfattning
I den här snabba handledningen tittade vi på den lata initialiseringen av objekt.
För det första såg vi hur man skapar en trådsäker lat initialisering i Java. Vi såg att det är mycket besvärligt och behöver mycket pannkodskod.
Därefter grävde vi in i Kotlin- lata nyckelord som används för lat initialisering av egenskaper. Till slut såg vi hur man kan skjuta upp tilldelning av variabler med hjälp av nyckelordet lateinit .
Implementeringen av alla dessa exempel och kodavsnitt finns på GitHub.