Uitgestelde initialisatie (lazy initialization) — een mechanisme in Kotlin waarbij de eigenschap van een object niet op het moment van aanmaken wordt geïnitialiseerd, maar bij de eerste toegang ertoe. Volgens gegevens van JetBrains, 2024, zijn lateinit en lazy twee ingebouwde hulpmiddelen voor het implementeren van deze strategie. Beide lossen het probleem van het uitstellen van initialisatie op, maar verschillen fundamenteel in werkingsmechanisme en toepassingsgebied.
Belangrijkste
Uitgestelde initialisatie — is een patroon waarbij een klasse-eigenschap zijn waarde niet op het moment van het construeren van het object krijgt, maar later, op verzoek. In Kotlin is dit patroon op twee fundamenteel verschillende manieren geïmplementeerd: de modifier lateinit en de delegate lazy.
Beide mechanismen lossen een gemeenschappelijk probleem op — de eigenschap moet in de klasse bestaan, maar de waarde ervan is ofwel nog niet bekend op het moment van het aanmaken van het object, ofwel de berekening ervan is te resource-intensief om zonder noodzaak uit te voeren. Volgens Google I/O 2023 kan tot 40% van de eigenschappen in een typische Android-app worden geoptimaliseerd door uitgestelde initialisatie, wat de opstarttijd met 15–25% vermindert.
De keuze tussen lateinit en lazy wordt bepaald door drie factoren: de veranderlijkheid van de eigenschap (var of val), de levensduur ervan (eenmalige of meermalige toewijzing) en de vereisten voor threadveiligheid (enkel- of meerdraads toegang).
Het eerste en meest voorkomende scenario — Dependency Injection. Het framework (Dagger, Hilt, Koin) injecteert afhankelijkheden na het aanmaken van het object, dus de eigenschap kan niet in de constructor worden geïnitialiseerd. Zonder lateinit zouden we alle afhankelijkheden als nullable moeten declareren en ze bij elk gebruik moeten controleren.
Het tweede scenario — zware resources: database, netwerkclient, bestandsbeheerder. Het aanmaken ervan kost tijd en geheugen, dus ze zouden alleen bij feitelijk gebruik moeten worden geïnitialiseerd. lazy is ideaal voor dergelijke gevallen en garandeert eenmalig aanmaken.
De derde situatie — Android-componenten (Activity, Fragment, ViewModel), waarvan de levenscyclus door het besturingssysteem wordt beheerd. Eigenschappen die afhankelijk zijn van onCreate, onViewCreated of het init-blok van ViewModel, kunnen niet in de constructor worden geïnitialiseerd.
lateinit — is een modifier voor var-eigenschappen die de Kotlin-compiler toestaat de initialisatie uit te stellen. De compiler vereist geen toewijzing van een waarde in de constructor, maar genereert een runtime-controle bij elke toegang: als de eigenschap niet is geïnitialiseerd, wordt UninitializedPropertyAccessException gegooid.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Beperkingen van lateinit: de eigenschap moet worden gedeclareerd als var (niet val), niet-nullable, niet-primitief type (Int, Double, Boolean enz.). De reden — primitieve typen worden gecompileerd naar JVM-primitieven, die geen status «niet geïnitialiseerd» hebben. Voor nullable-eigenschappen is uitgestelde initialisatie niet nodig: null betekent al de afwezigheid van een waarde.
Voor het controleren van de status van een lateinit-eigenschap wordt een ingebouwde verwijzing via de operator :: gebruikt: ::propertyName.isInitialized. Dit is de enige veilige manier om te controleren of een eigenschap is geïnitialiseerd, zonder het risico een uitzondering te krijgen. De controle is alleen beschikbaar vanuit dezelfde klasse of een innerlijke klasse, niet vanuit externe code.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit voegt geen overhead toe na initialisatie: na toewijzing van de waarde is toegang tot de eigenschap identiek aan directe verwijzing naar het veld. De enige kosten — controle op initialisatie bij elke lezing vóór toewijzing. Na initialisatie optimaliseert de JIT-compiler de controle.
Een belangrijk kenmerk: lateinit-eigenschappen kunnen niet worden gebruikt in inline klassen en worden niet ondersteund voor eigenschappen met aangepaste getter/setter. Als een eigenschap berekende toegang vereist — gebruik dan lazy in plaats van lateinit.
lazy — is een eigenschapsdelegate, ingebouwd in de standaard Kotlin-bibliotheek. Het berekent de waarde bij de eerste toegang tot de eigenschap en cacht het resultaat voor alle volgende aanroepen. In tegenstelling tot lateinit werkt lazy alleen met val, waardoor de eigenschap onveranderlijk wordt na initialisatie.
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 accepteert een optionele parameter LazyThreadSafetyMode, die het mechanisme voor threadveiligheid regelt. Standaard wordt SYNCHRONIZED gebruikt — dubbele controle met vergrendeling (Double-Checked Locking), die eenmalige initialisatie garandeert, zelfs bij gelijktijdige toegang vanuit meerdere draden.
De modus PUBLICATION staat parallelle initialisatie toe: meerdere draden kunnen tegelijkertijd het initialisatieblok uitvoeren, maar het resultaat wordt alleen geaccepteerd van de eerste die voltooit. Dit is sneller dan SYNCHRONIZED bij hoge concurrentie, maar verhoogt het resourceverbruik.
De modus NONE schakelt synchronisatie volledig uit. Gebruik het alleen voor eigenschappen waarvan de toegang gegarandeerd vanuit één draad plaatsvindt. In deze modus werkt lazy met minimale overhead — praktisch als directe toewijzing.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy is de juiste keuze voor eenmalig geïnitialiseerde afhankelijkheden: repositories, netwerkclients, caches, databases. De semantiek van val beschermt tegen per ongeluk overschrijven en standaard threadveiligheid maakt de code veilig in een multi-thread omgeving. lazy werkt ook correct met primitieve typen, wat onmogelijk is met lateinit.
In Android wordt lazy vaak gebruikt voor het initialiseren van ViewModel-afhankelijkheden via by viewModels() of voor het aanmaken van Retrofit-clients. Wees echter voorzichtig: als een lazy-blok een verwijzing naar Activity of Fragment vastlegt, kan dit leiden tot geheugenlekken, omdat de delegate de closure bewaart tot het einde van de levensduur van de eigenschap.
De keuze tussen lateinit en lazy — is geen kwestie van voorkeur, maar een architectonische beslissing die wordt bepaald door de aard van de eigenschap. Elk mechanisme lost zijn eigen taak op en hun toepassingsgebieden overlappen slechts gedeeltelijk.
| Criterium | lateinit | lazy |
|---|---|---|
| Eigenschapstype | alleen var | alleen val |
| Nullable | verboden | toegestaan |
| Primitieve typen | verboden | toegestaan |
| Threadveiligheid | niet gegarandeerd | SYNCHRONIZED standaard |
| Statuscontrole | ::x.isInitialized | niet vereist |
| Uitzondering bij fout | UninitializedPropertyAccessException | fout in initialisatieblok |
| Caching | niet toegepast | eenmalige berekening |
| Android Binding | View Binding, Data Binding | niet gebruikt |
| DI-frameworks | Dagger, Hilt, Koin | handmatige injectie |
Gebruik lateinit wanneer de eigenschap moet veranderen na initialisatie of het aanmaken ervan door externe code wordt beheerd. Een typisch voorbeeld — View Binding in Android Activity: binding wordt aangemaakt in onCreate, maar blijft var, omdat het framework val niet ondersteunt voor dit scenario.
Gebruik lazy wanneer de eigenschap eenmalig wordt geïnitialiseerd, de berekening ervan duur is en de waarde niet verandert tijdens de levensduur van het object. Een klassiek voorbeeld — het lui aanmaken van een Retrofit-client of Room-database bij de eerste toegang tot de repository.
In één klasse kunnen beide mechanismen tegelijkertijd worden gebruikt. Bijvoorbeeld lateinit voor View Binding en lazy voor een repository. Dit is normale praktijk, die verschillende vereisten voor verschillende eigenschappen weerspiegelt. Het belangrijkste — verwar de semantiek niet: gebruik lateinit niet waar val nodig is en gebruik lazy niet voor eigenschappen die overschreven moeten worden.
De meest voorkomende fout met lateinit — toegang tot de eigenschap voordat deze is geïnitialiseerd. Dit leidt tot UninitializedPropertyAccessException, die niet wordt opgevangen in de compilatiefase, omdat Kotlin de ontwikkelaar vertrouwt op de juiste initialisatievolgorde. Oplossing — controleer altijd de status via ::property.isInitialized vóór toegang in onduidelijke situaties.
Een tweede veelvoorkomend probleem — het gebruik van lateinit voor eigenschappen die semantisch val zijn. Als de waarde eenmaal wordt ingesteld en niet meer verandert, is lazy de juistere keuze. Het maakt de eigenschap onveranderlijk, sluit per ongeluk overschrijven uit en voegt gratis threadveiligheid toe.
De derde fout — lazy met bijwerkingen. Een lazy-initialisatieblok mag geen externe status wijzigen of vertrouwen op de initialisatievolgorde van andere lazy-eigenschappen, omdat de volgorde van berekeningen afhangt van de eerste toegang en onduidelijk kan zijn. Als lazy-eigenschappen naar elkaar verwijzen, leidt dit tot een cyclische afhankelijkheid en StackOverflowError.
Het vierde probleem — geheugenlek via lazy in Android. Als een lazy-blok een verwijzing naar Activity of Fragment vastlegt — bewaart de delegate de closure, en kan de garbage collector de component niet vrijgeven, zelfs niet na vernietiging ervan. Oplossing — gebruik lazy alleen met kortlevende objecten of geef de Application-context door, niet Activity.
Een vijfde veelgemaakte fout — het proberen toepassen van lateinit op primitieve typen. De Kotlin-compiler blokkeert dit op syntaxniveau, maar ontwikkelaars proberen de beperking te omzeilen via nullable-wrappers. Dit leidt tot overbodige null-controles en maakt de voordelen van uitgestelde initialisatie volledig teniet.
Veelgestelde vragen
lateinit — modifier voor var-eigenschappen, die initialisatie na de constructor toestaat. lazy — delegate voor val-eigenschappen, die de waarde bij de eerste toegang berekent en cacht. lateinit ondersteunt geen primitieve typen en nullable, terwijl lazy standaard thread-safe is.
Ja, via de ingebouwde verwijzing naar de eigenschap: ::propertyName.isInitialized. De methode retourneert true als de eigenschap al is geïnitialiseerd. Dit is de enige veilige manier om UninitializedPropertyAccessException te voorkomen bij het werken met lateinit-velden.
Primitieve typen — Int, Double, Boolean en andere — worden gecompileerd naar JVM-primitieven (int, double, boolean) die geen status «niet geïnitialiseerd» hebben. lateinit gebruikt null als vlag en primitieven kunnen niet null zijn, daarom is het mechanisme fysiek onuitvoerbaar voor deze typen.
Standaard wordt LazyThreadSafetyMode.SYNCHRONIZED gebruikt — dubbele controle met vergrendeling, die eenmalige initialisatie garandeert bij toegang vanuit meerdere draden. Voor single-thread scenario's gebruik je NONE, voor hoge concurrentie — PUBLICATION.
Wanneer de eigenschap moet veranderen na initialisatie of het aanmaken ervan door het framework wordt beheerd. Een typisch voorbeeld — View Binding in Android Activity: binding wordt aangemaakt in onCreate en moet var zijn. Voor eenmalig geïnitialiseerde val-afhankelijkheden gebruik je lazy.
Samenvatting
We ontwikkelen een mobiele applicatie turnkey
IT Sectr creëert sinds 2017 iOS- en Android-applicaties voor startups en bedrijven. We adviseren u en stellen de beste oplossing voor.
Lees ook