L’inizializzazione differita (lazy initialization) è un meccanismo in Kotlin in cui una proprietà di un oggetto viene inizializzata non al momento della creazione, ma al primo accesso. Secondo JetBrains, 2024, lateinit e lazy sono due strumenti integrati per implementare questa strategia. Entrambi risolvono il problema dell’inizializzazione differita, ma differiscono fondamentalmente nel meccanismo di funzionamento e nell’ambito di applicazione.
Punti chiave
L’inizializzazione differita è un modello in cui una proprietà di classe riceve il suo valore non al momento della costruzione dell’oggetto, ma successivamente, su richiesta. In Kotlin, questo modello viene implementato in due modi fondamentalmente diversi: il modificatore lateinit e il delegato lazy.
Entrambi i meccanismi risolvono un problema comune — una proprietà deve esistere nella classe, ma il suo valore o è sconosciuto al momento della creazione dell’oggetto, oppure il suo calcolo è troppo dispendioso in termini di risorse per essere eseguito inutilmente. Secondo Google I/O 2023, fino al 40% delle proprietà in una tipica applicazione Android può essere ottimizzato attraverso l’inizializzazione differita, riducendo il tempo di avvio del 15–25%.
La scelta tra lateinit e lazy è determinata da tre fattori: la mutabilità della proprietà (var o val), il suo ciclo di vita (assegnazione singola o multipla) e i requisiti di thread-safety (accesso single-thread o multi-thread).
Il primo e più comune scenario è l’Iniezione di Dipendenze. Il framework (Dagger, Hilt, Koin) inietta le dipendenze dopo la creazione dell’oggetto, quindi la proprietà non può essere inizializzata nel costruttore. Senza lateinit, tutte le dipendenze dovrebbero essere dichiarate nullable e controllate a ogni utilizzo.
Il secondo scenario riguarda le risorse pesanti: database, client di rete, gestori di file. La loro creazione richiede tempo e memoria, quindi dovrebbero essere inizializzate solo quando effettivamente utilizzate. lazy è ideale per questi casi, garantendo una creazione unica.
La terza situazione riguarda i componenti Android (Activity, Fragment, ViewModel) il cui ciclo di vita è gestito dal sistema operativo. Le proprietà che dipendono da onCreate, onViewCreated o dal blocco init del ViewModel non possono essere inizializzate nel costruttore.
lateinit è un modificatore per proprietà var che consente al compilatore Kotlin di posticipare l’inizializzazione. Il compilatore non richiede un’assegnazione di valore nel costruttore, ma genera un controllo in fase di esecuzione a ogni accesso: se la proprietà non è inizializzata, lancia UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Limitazioni di lateinit: la proprietà deve essere dichiarata come var (non val), non nullable e non di tipo primitivo (Int, Double, Boolean, ecc.). Il motivo è che i tipi primitivi vengono compilati in primitivi JVM, che non hanno uno stato “non inizializzato”. Per le proprietà nullable, l’inizializzazione differita non è necessaria: null significa già assenza di valore.
Per verificare lo stato di una proprietà lateinit, utilizzare il riferimento incorporato tramite l’operatore ::: ::propertyName.isInitialized. Questo è l’unico modo sicuro per verificare se una proprietà è stata inizializzata senza rischiare un’eccezione. Il controllo è disponibile solo dalla stessa classe o da una classe interna, non da codice esterno.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit non aggiunge overhead dopo l’inizializzazione: una volta assegnato il valore, l’accesso alla proprietà è identico all’accesso diretto al campo. L’unico costo è il controllo di inizializzazione a ogni lettura prima dell’assegnazione. Dopo l’inizializzazione, il compilatore JIT ottimizza il controllo.
Una nota importante: le proprietà lateinit non possono essere utilizzate nelle classi inline e non sono supportate per proprietà con getter/setter personalizzati. Se una proprietà richiede un accesso calcolato, utilizzare lazy invece di lateinit.
lazy è un delegato di proprietà integrato nella libreria standard di Kotlin. Calcola il valore al primo accesso alla proprietà e memorizza nella cache il risultato per tutte le chiamate successive. A differenza di lateinit, lazy funziona solo con val, rendendo la proprietà immutabile dopo l’inizializzazione.
class UserRepository {
private val database: Database by lazy {
Database.create("users.db")
}
fun getUser(id: String): User {
return database.query("SELECT * FROM users WHERE id = ?", id)
}
}
lazy accetta un parametro opzionale LazyThreadSafetyMode che controlla il meccanismo di thread-safety. Il valore predefinito è SYNCHRONIZED — doppio controllo con blocco, che garantisce un’inizializzazione unica anche in caso di accesso concorrente da più thread.
La modalità PUBLICATION consente l’inizializzazione parallela: più thread possono eseguire simultaneamente il blocco di inizializzazione, ma il risultato viene accettato solo dal primo che completa. È più veloce di SYNCHRONIZED in caso di contesa elevata, ma aumenta il consumo di risorse.
La modalità NONE disabilita completamente la sincronizzazione. Usarla solo per proprietà il cui accesso è garantito da un singolo thread. In questa modalità, lazy opera con overhead minimo — quasi come un’assegnazione diretta.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy è la scelta giusta per dipendenze inizializzate una volta: repository, client di rete, cache, database. La semantica di val protegge dalla sovrascrittura accidentale e la thread-safety predefinita rende il codice sicuro in ambienti multi-thread. lazy funziona correttamente anche con i tipi primitivi, cosa impossibile con lateinit.
In Android, lazy viene spesso utilizzato per inizializzare le dipendenze di ViewModel tramite by viewModels() o per creare client Retrofit. Tuttavia, fare attenzione: se un blocco lazy cattura un riferimento a un’Activity o un Fragment, può causare una perdita di memoria, poiché il delegato mantiene la chiusura per tutta la durata della proprietà.
La scelta tra lateinit e lazy non è una questione di preferenza, ma una decisione architettonica determinata dalla natura della proprietà. Ogni meccanismo risolve il proprio compito e le loro aree di applicazione si sovrappongono solo parzialmente.
| Criterio | lateinit | lazy |
|---|---|---|
| Tipo di proprietà | solo var | solo val |
| Nullable | non consentito | consentito |
| Tipi primitivi | non consentiti | consentiti |
| Thread-safety | non garantita | SYNCHRONIZED predefinito |
| Controllo stato | ::x.isInitialized | non necessario |
| Eccezione su errore | UninitializedPropertyAccessException | errore nel blocco init |
| Cache | non applicabile | calcolo singolo |
| Android Binding | View Binding, Data Binding | non utilizzato |
| Framework DI | Dagger, Hilt, Koin | iniezione manuale |
Utilizzare lateinit quando una proprietà deve cambiare dopo l’inizializzazione o la sua creazione è gestita da codice esterno. Un esempio tipico è View Binding in Android Activity: il binding viene creato in onCreate ma rimane var perché il framework non supporta val per questo scenario.
Utilizzare lazy quando una proprietà viene inizializzata una volta, il suo calcolo è costoso e il valore non cambia durante la vita dell’oggetto. Un esempio classico è la creazione lazy di un client Retrofit o di un database Room al primo accesso al repository.
Entrambi i meccanismi possono essere utilizzati simultaneamente all’interno della stessa classe. Ad esempio, lateinit per View Binding e lazy per un repository. Questa è una pratica normale che riflette requisiti diversi per proprietà diverse. L’importante è non confondere la semantica: non utilizzare lateinit dove serve val, e non utilizzare lazy per proprietà che devono essere riassegnate.
L’errore più comune con lateinit è accedere alla proprietà prima che venga inizializzata. Ciò porta a UninitializedPropertyAccessException, che non viene intercettata in fase di compilazione perché Kotlin si fida che lo sviluppatore garantisca l’ordine corretto di inizializzazione. La soluzione è controllare sempre lo stato tramite ::property.isInitialized prima dell’accesso in situazioni ambigue.
Il secondo problema comune è utilizzare lateinit per proprietà che sono semanticamente val. Se il valore viene impostato una volta e non cambia mai, lazy è la scelta più corretta. Rende la proprietà immutabile, previene la sovrascrittura accidentale e aggiunge thread-safety gratuitamente.
Il terzo errore è lazy con effetti collaterali. Il blocco di inizializzazione lazy non dovrebbe modificare lo stato esterno né dipendere dall’ordine di inizializzazione di altre proprietà lazy, poiché la sequenza di calcolo dipende dal primo accesso e potrebbe non essere ovvia. Se le proprietà lazy si riferiscono l’una all’altra, si crea una dipendenza circolare e StackOverflowError.
Il quarto problema riguarda le perdite di memoria tramite lazy in Android. Se un blocco lazy cattura un riferimento a un’Activity o un Fragment, il delegato mantiene la chiusura e il garbage collector non può liberare il componente nemmeno dopo la sua distruzione. La soluzione è usare lazy solo con oggetti a vita breve o passare il contesto di Application invece dell’Activity.
Il quinto errore tipico è tentare di applicare lateinit ai tipi primitivi. Il compilatore Kotlin lo blocca a livello di sintassi, ma gli sviluppatori tentano di aggirare la limitazione attraverso wrapper nullable. Ciò porta a controlli null non necessari e annulla completamente i vantaggi dell’inizializzazione differita.
Domande frequenti
lateinit è un modificatore per proprietà var, che consente l’inizializzazione dopo il costruttore. lazy è un delegato per proprietà val, che calcola il valore al primo accesso e lo memorizza nella cache. lateinit non supporta i tipi primitivi e nullable, mentre lazy è thread-safe per impostazione predefinita.
Sì, tramite il riferimento incorporato alla proprietà: ::propertyName.isInitialized. Il metodo restituisce true se la proprietà è già stata inizializzata. Questo è l’unico modo sicuro per evitare UninitializedPropertyAccessException quando si lavora con campi lateinit.
I tipi primitivi — Int, Double, Boolean e altri — vengono compilati in primitivi JVM (int, double, boolean), che non hanno uno stato “non inizializzato”. lateinit usa null come flag e i primitivi non possono essere null, quindi il meccanismo è fisicamente impossibile per questi tipi.
Il valore predefinito è LazyThreadSafetyMode.SYNCHRONIZED — doppio controllo con blocco, che garantisce un’inizializzazione unica in caso di accesso concorrente da più thread. Per scenari single-thread, utilizzare NONE; per contesa elevata, utilizzare PUBLICATION.
Quando la proprietà deve cambiare dopo l’inizializzazione o la sua creazione è gestita dal framework. Un esempio tipico è View Binding in Android Activity: il binding viene creato in onCreate e deve essere var. Per dipendenze val inizializzate una volta, utilizzare lazy.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche