lateinit / lazy: l’essenza dell’inizializzazione differita e i suoi meccanismi in Kotlin

Autore: IT Sectr Pubblicato: 2026-06-23 Tempo di lettura: 8 min

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

  • lateinit — un modificatore per proprietà var, che consente l’inizializzazione dopo la creazione dell’oggetto
  • lazy — un delegato per proprietà val, che inizializza il valore al primo accesso
  • lateinit richiede var e non supporta i tipi primitivi JVM
  • lazy è thread-safe per impostazione predefinita e memorizza nella cache il risultato calcolato
  • lateinit lancia UninitializedPropertyAccessException in caso di accesso prematuro

Cos’è l’inizializzazione differita in Kotlin?

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).

Quando viene utilizzata l’inizializzazione differita

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: meccanismo e limitazioni

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.

kotlin
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.

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

    fun isReady(): Boolean {
        return ::binding.isInitialized
    }
}

Prestazioni di lateinit

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: meccanismo e vantaggi

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.

kotlin
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.

Modalità di thread-safety di lazy

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.

kotlin
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
    Config.loadFromFile("config.json")
}

Quando lazy è preferibile

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à.

lateinit vs lazy: confronto degli approcci

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.

Criteriolateinitlazy
Tipo di proprietàsolo varsolo val
Nullablenon consentitoconsentito
Tipi primitivinon consentiticonsentiti
Thread-safetynon garantitaSYNCHRONIZED predefinito
Controllo stato::x.isInitializednon necessario
Eccezione su erroreUninitializedPropertyAccessExceptionerrore nel blocco init
Cachenon applicabilecalcolo singolo
Android BindingView Binding, Data Bindingnon utilizzato
Framework DIDagger, Hilt, Koininiezione 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.

Combinazione di lateinit e lazy

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.

Errori comuni nell’uso di lateinit e lazy

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

Qual è la differenza tra lateinit e lazy in Kotlin?

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.

Posso verificare se una proprietà lateinit è inizializzata?

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.

Perché lateinit non può essere utilizzato con i tipi primitivi?

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.

Qual è la modalità di thread-safety predefinita di lazy?

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 dovrei usare lateinit invece di lazy in Android?

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

  • lateinit — un modificatore per proprietà var di Kotlin, che consente l’inizializzazione dopo il costruttore senza nullable
  • lazy — un delegato di proprietà per val con calcolo singolo e memorizzazione automatica nella cache del risultato
  • lateinit lancia UninitializedPropertyAccessException all’accesso prima dell’inizializzazione
  • lazy è thread-safe per impostazione predefinita tramite LazyThreadSafetyMode.SYNCHRONIZED
  • lateinit è incompatibile con i tipi primitivi e le proprietà nullable
  • lazy può causare perdite di memoria in Android quando cattura il contesto in una chiusura
  • Scegli lateinit per proprietà mutabili e lazy per dipendenze val inizializzate una volta

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.

Discuti il progetto

Leggi anche