Inițializarea întârziată (lazy initialization) — un mecanism în Kotlin prin care proprietatea unui obiect este inițializată nu la momentul creării, ci la prima accesare. Conform datelor JetBrains, 2024, lateinit și lazy — două instrumente încorporate pentru implementarea acestei strategii. Ambele rezolvă problema amânării inițializării, dar diferă fundamental prin mecanismul de funcționare și domeniul de aplicare.
Principalele
Inițializarea întârziată — este un model în care proprietatea unei clase primește valoarea nu la momentul construirii obiectului, ci mai târziu, la cerere. În Kotlin, acest model este implementat în două moduri fundamental diferite: modificatorul lateinit și delegatul lazy.
Ambele mecanisme rezolvă o problemă comună — proprietatea trebuie să existe în clasă, dar valoarea sa fie nu este încă cunoscută la momentul creării obiectului, fie calculul său este prea costisitor pentru a fi executat fără necesitate. Conform datelor Google I/O 2023, până la 40% din proprietățile dintr-o aplicație Android tipică pot fi optimizate prin inițializare întârziată, ceea ce reduce timpul de pornire cu 15–25%.
Alegerea între lateinit și lazy este determinată de trei factori: modificabilitatea proprietății (var sau val), durata sa de viață (atribuire unică sau multiplă) și cerințele de siguranță a firelor de execuție (acces mono sau multi-thread).
Primul și cel mai frecvent scenariu — Dependency Injection. Framework-ul (Dagger, Hilt, Koin) injectează dependențele după crearea obiectului, prin urmare proprietatea nu poate fi inițializată în constructor. Fără lateinit ar trebui să declarăm toate dependențele ca nullable și să le verificăm la fiecare utilizare.
Al doilea scenariu — resurse grele: baza de date, client de rețea, manager de fișiere. Crearea lor necesită timp și memorie, de aceea ar trebui să fie inițializate doar la utilizarea efectivă. lazy este ideal pentru astfel de cazuri, garantând crearea unică.
A treia situație — componente Android (Activity, Fragment, ViewModel), al căror ciclu de viață este gestionat de sistemul de operare. Proprietățile care depind de onCreate, onViewCreated sau blocul init al ViewModel nu pot fi inițializate în constructor.
lateinit — este un modificator pentru proprietăți var care permite compilatorului Kotlin să amâne inițializarea. Compilatorul nu cere atribuirea valorii în constructor, dar generează o verificare la runtime la fiecare acces: dacă proprietatea nu este inițializată, se aruncă UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Limitări ale lateinit: proprietatea trebuie declarată ca var (nu val), non-nullable, de tip non-primitiv (Int, Double, Boolean etc.). Motivul — tipurile primitive se compilează în primitive JVM, care nu au starea «neinițializat». Pentru proprietățile nullable inițializarea întârziată nu este necesară: null înseamnă deja absența valorii.
Pentru verificarea stării proprietății lateinit se folosește referința încorporată prin operatorul ::: ::propertyName.isInitialized. Aceasta este singura modalitate sigură de a verifica dacă proprietatea este inițializată, fără riscul de a primi o excepție. Verificarea este disponibilă doar din aceeași clasă sau dintr-o clasă internă, nu din cod extern.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit nu adaugă costuri suplimentare după inițializare: după atribuirea valorii, accesul la proprietate este identic cu referința directă la câmp. Singurul cost — verificarea inițializării la fiecare citire înainte de atribuire. După inițializare, compilatorul JIT optimizează verificarea.
O caracteristică importantă: proprietățile lateinit nu pot fi folosite în clase inline și nu sunt suportate pentru proprietăți cu getter/setter personalizate. Dacă proprietatea necesită acces calculat — folosiți lazy în loc de lateinit.
lazy — este un delegat de proprietate, încorporat în biblioteca standard Kotlin. Acesta calculează valoarea la prima accesare a proprietății și cache-uiește rezultatul pentru toate apelurile ulterioare. Spre deosebire de lateinit, lazy funcționează doar cu val, făcând proprietatea imutabilă după inițializare.
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 acceptă un parametru opțional LazyThreadSafetyMode, care controlează mecanismul de siguranță a firelor de execuție. Implicit se folosește SYNCHRONIZED — verificare dublă cu blocare (Double-Checked Locking), garantând inițializarea unică chiar și la accesul simultan din mai multe fire.
Modul PUBLICATION permite inițializarea paralelă: mai multe fire pot executa simultan blocul de inițializare, dar rezultatul este acceptat doar de la primul care finalizează. Este mai rapid decât SYNCHRONIZED la concurență ridicată, dar consumă mai multe resurse.
Modul NONE dezactivează complet sincronizarea. Folosiți-l doar pentru proprietăți al căror acces are loc garantat dintr-un singur fir. În acest mod, lazy funcționează cu costuri minime — practic ca o atribuire directă.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy este alegerea corectă pentru dependențele inițializate o singură dată: repository-uri, clienți de rețea, cache-uri, baze de date. Semantica val protejează împotriva suprascrierii accidentale, iar siguranța implicită a firelor face codul sigur în medii multi-thread. lazy funcționează corect și cu tipuri primitive, ceea ce este imposibil cu lateinit.
În Android, lazy este adesea folosit pentru inițializarea dependențelor ViewModel prin by viewModels() sau pentru crearea clienților Retrofit. Totuși, fiți atenți: dacă blocul lazy capturează o referință către Activity sau Fragment, aceasta poate duce la scurgeri de memorie, deoarece delegatul păstrează închiderea până la sfârșitul vieții proprietății.
Alegerea între lateinit și lazy — nu este o chestiune de preferință, ci o decizie arhitecturală determinată de natura proprietății. Fiecare mecanism își rezolvă sarcina, iar domeniile lor de aplicare se suprapun doar parțial.
| Criteriu | lateinit | lazy |
|---|---|---|
| Tipul proprietății | doar var | doar val |
| Nullable | interzis | permis |
| Tipuri primitive | interzise | permise |
| Siguranța firelor | nu este garantată | SYNCHRONIZED implicit |
| Verificarea stării | ::x.isInitialized | nu este necesară |
| Excepție la eroare | UninitializedPropertyAccessException | eroare în blocul de inițializare |
| Cache | nu se aplică | calcul unic |
| Android Binding | View Binding, Data Binding | nu se folosește |
| Frameworkuri DI | Dagger, Hilt, Koin | injectare manuală |
Folosiți lateinit atunci când proprietatea trebuie să se schimbe după inițializare sau crearea sa este gestionată de cod extern. Exemplul tipic — View Binding în Android Activity: binding este creat în onCreate, dar rămâne var, deoarece framework-ul nu suportă val pentru acest scenariu.
Folosiți lazy atunci când proprietatea este inițializată o singură dată, calculul său este costisitor și valoarea nu se schimbă pe durata vieții obiectului. Exemplul clasic — crearea lentă a clientului Retrofit sau a bazei de date Room la prima accesare a repository-ului.
Într-o singură clasă pot fi folosite simultan ambele mecanisme. De exemplu, lateinit pentru View Binding și lazy pentru repository. Aceasta este o practică normală, reflectând cerințe diferite pentru proprietăți diferite. Principalul — nu confundați semantica: nu folosiți lateinit acolo unde este necesar val și nu folosiți lazy pentru proprietăți care trebuie suprascrise.
Cea mai frecventă eroare cu lateinit — accesarea proprietății înainte de inițializarea acesteia. Aceasta duce la UninitializedPropertyAccessException, care nu este prins în faza de compilare, deoarece Kotlin are încredere în dezvoltator pentru succesiunea corectă a inițializării. Soluția — verificați întotdeauna starea prin ::property.isInitialized înainte de acces în situații ambigue.
A doua problemă frecventă — utilizarea lateinit pentru proprietăți care semantic sunt val. Dacă valoarea este setată o dată și nu se mai schimbă, lazy este alegerea mai corectă. Face proprietatea imutabilă, exclude suprascrierea accidentală și adaugă siguranța firelor gratuit.
A treia eroare — lazy cu efecte secundare. Blocul de inițializare lazy nu ar trebui să modifice starea externă sau să se bazeze pe ordinea de inițializare a altor proprietăți lazy, deoarece succesiunea calculelor depinde de prima accesare și poate fi neevidentă. Dacă proprietățile lazy se referă una la alta, aceasta duce la o dependență ciclică și StackOverflowError.
A patra problemă — scurgere de memorie prin lazy în Android. Dacă blocul lazy capturează o referință către Activity sau Fragment — delegatul păstrează închiderea, iar colectorul de gunoi nu poate elibera componenta chiar și după distrugerea acesteia. Soluția — folosiți lazy doar cu obiecte cu viață scurtă sau transmiteți contextul Application, nu Activity.
A cincea eroare tipică — încercarea de a aplica lateinit tipurilor primitive. Compilatorul Kotlin blochează aceasta la nivel de sintaxă, dar dezvoltatorii încearcă să ocolească limitarea prin intermediul wrapper-elor nullable. Aceasta duce la verificări suplimentare de null și anulează complet avantajele inițializării întârziate.
Întrebări frecvente
lateinit — modificator pentru proprietăți var, care permite inițializarea după constructor. lazy — delegat pentru proprietăți val, care calculează valoarea la prima accesare și o cache-uiește. lateinit nu suportă tipuri primitive și nullable, iar lazy este implicit thread-safe.
Da, prin referința încorporată la proprietate: ::propertyName.isInitialized. Metoda returnează true dacă proprietatea este deja inițializată. Aceasta este singura modalitate sigură de a evita UninitializedPropertyAccessException la lucrul cu câmpuri lateinit.
Tipurile primitive — Int, Double, Boolean și altele — se compilează în primitive JVM (int, double, boolean) care nu au starea «neinițializat». lateinit folosește null ca un indicator, iar primitivele nu pot fi null, prin urmare mecanismul este fizic nerealizabil pentru aceste tipuri.
Implicit se folosește LazyThreadSafetyMode.SYNCHRONIZED — verificare dublă cu blocare, garantând inițializarea unică la accesul din mai multe fire. Pentru scenarii single-thread folosiți NONE, pentru concurență ridicată — PUBLICATION.
Când proprietatea trebuie să se schimbe după inițializare sau crearea sa este gestionată de framework. Exemplul tipic — View Binding în Android Activity: binding este creat în onCreate și trebuie să fie var. Pentru dependențe val inițializate o singură dată folosiți lazy.
Rezumat
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și