Inizializzazione pigra in Kotlin

1. Panoramica

In questo articolo, esamineremo una delle funzionalità più interessanti della sintassi di Kotlin: l'inizializzazione pigra.

Esamineremo anche la parola chiave lateinit che ci consente di ingannare il compilatore e inizializzare i campi non nulli nel corpo della classe, invece che nel costruttore.

2. Pattern di inizializzazione pigro in Java

A volte abbiamo bisogno di costruire oggetti che hanno un complicato processo di inizializzazione. Inoltre, spesso non possiamo essere sicuri che l'oggetto, per il quale abbiamo pagato il costo di inizializzazione all'inizio del nostro programma, verrà utilizzato nel nostro programma.

Il concetto di "inizializzazione lenta" è stato progettato per impedire l'inizializzazione non necessaria degli oggetti . In Java, creare un oggetto in modo pigro e thread-safe non è una cosa facile da fare. Pattern come Singleton hanno difetti significativi nel multithreading, nei test, ecc. E ora sono ampiamente conosciuti come anti-pattern da evitare.

In alternativa, possiamo sfruttare l'inizializzazione statica dell'oggetto interno in Java per ottenere la pigrizia:

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

Si noti come, solo quando chiameremo il metodo getInstance () su ClassWithHeavyInitialization , verrà caricata la classe statica LazyHolder e verrà creata la nuova istanza di ClassWithHeavyInitialization . Successivamente, l'istanza verrà assegnata al riferimento INSTANCE finale statico .

Possiamo verificare che getInstance () restituisca la stessa istanza ogni volta che viene chiamato:

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

Tecnicamente è OK ma ovviamente un po 'troppo complicato per un concetto così semplice .

3. Inizializzazione pigra in Kotlin

Possiamo vedere che l'uso del pattern di inizializzazione pigro in Java è piuttosto complicato. Abbiamo bisogno di scrivere molto codice boilerplate per raggiungere il nostro obiettivo. Fortunatamente, il linguaggio Kotlin ha il supporto integrato per l'inizializzazione lenta .

Per creare un oggetto che verrà inizializzato al primo accesso ad esso, possiamo utilizzare il metodo 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) }

Come possiamo vedere, il lambda passato alla funzione lazy è stato eseguito una sola volta.

Quando accediamo a lazyValue per la prima volta, si è verificata un'inizializzazione effettiva e l'istanza restituita della classe ClassWithHeavyInitialization è stata assegnata al riferimento lazyValue . L'accesso successivo a lazyValue ha restituito l'oggetto precedentemente inizializzato.

Possiamo passare LazyThreadSafetyMode come argomento alla funzione lazy . La modalità di pubblicazione predefinita è SYNCHRONIZED , il che significa che solo un singolo thread può inizializzare l'oggetto specificato.

Possiamo passare una PUBBLICAZIONE come modalità, il che farà sì che ogni thread possa inizializzare una determinata proprietà. L'oggetto assegnato al riferimento sarà il primo valore restituito, quindi il primo thread vince.

Diamo uno sguardo a quello scenario:

@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) }

Possiamo vedere che l'avvio di due thread contemporaneamente causa l'inizializzazione di ClassWithHeavyInitialization due volte.

C'è anche una terza modalità - NONE - ma non dovrebbe essere usata nell'ambiente multithread poiché il suo comportamento non è definito.

4. Lateinit di Kotlin

In Kotlin, ogni proprietà della classe non nullable dichiarata nella classe deve essere inizializzata nel costruttore o come parte della dichiarazione della variabile. Se non riusciamo a farlo, il compilatore Kotlin si lamenterà con un messaggio di errore:

Kotlin: Property must be initialized or be abstract

Ciò significa fondamentalmente che dovremmo inizializzare la variabile o contrassegnarla come astratta .

D'altra parte, ci sono alcuni casi in cui la variabile può essere assegnata dinamicamente, ad esempio mediante l'inserimento di dipendenze.

Per posticipare l'inizializzazione della variabile, possiamo specificare che un campo è lateinit . Stiamo informando il compilatore che questa variabile verrà assegnata in seguito e stiamo liberando il compilatore dalla responsabilità di assicurarsi che questa variabile venga inizializzata:

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

If we forget to initialize the lateinit property, we'll get an UninitializedPropertyAccessException:

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

It's worth mentioning that we can only use lateinit variables with non-primitive data types. Therefore, it's not possible to write something like this:

lateinit var value: Int

And if we do so, we would get a compilation error:

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

5. Conclusion

In this quick tutorial, we looked at the lazy initialization of objects.

Firstly, we saw how to create a thread-safe lazy initialization in Java. We saw that it is a very cumbersome and needs a lot of boilerplate code.

Successivamente, abbiamo approfondito la parola chiave lazy di Kotlin che viene utilizzata per l'inizializzazione lenta delle proprietà. Alla fine, abbiamo visto come differire l'assegnazione delle variabili utilizzando la parola chiave lateinit .

L'implementazione di tutti questi esempi e frammenti di codice può essere trovata su GitHub.