lateinit / lazy: essentie van uitgestelde initialisatie en mechanismen in Kotlin

Auteur: IT Sectr Gepubliceerd: 2026-06-23 Leestijd: 8 min

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

  • lateinit — modifier voor var-eigenschappen, die initialisatie na het aanmaken van het object toestaat
  • lazy — delegate voor val-eigenschappen, die de waarde bij de eerste toegang initialiseert
  • lateinit vereist var en ondersteunt geen primitieve JVM-typen
  • lazy is standaard thread-safe en cacht het berekende resultaat
  • lateinit gooit UninitializedPropertyAccessException bij voortijdige toegang

Wat is uitgestelde initialisatie in Kotlin?

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

Wanneer wordt uitgestelde initialisatie toegepast

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: mechanisme en beperkingen

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.

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

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

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

Prestaties van lateinit

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: mechanisme en voordelen

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.

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

Threadveiligheidsmodi van lazy

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.

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

Wanneer lazy de voorkeur heeft

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.

lateinit vs lazy: vergelijking van benaderingen

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.

Criteriumlateinitlazy
Eigenschapstypealleen varalleen val
Nullableverbodentoegestaan
Primitieve typenverbodentoegestaan
Threadveiligheidniet gegarandeerdSYNCHRONIZED standaard
Statuscontrole::x.isInitializedniet vereist
Uitzondering bij foutUninitializedPropertyAccessExceptionfout in initialisatieblok
Cachingniet toegepasteenmalige berekening
Android BindingView Binding, Data Bindingniet gebruikt
DI-frameworksDagger, Hilt, Koinhandmatige 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.

Combineren van lateinit en lazy

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.

Veelgemaakte fouten bij het gebruik van lateinit en lazy

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

Wat is het verschil tussen lateinit en lazy in Kotlin?

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.

Kan worden gecontroleerd of een lateinit-eigenschap is geïnitialiseerd?

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.

Waarom kan lateinit niet worden gebruikt met primitieve typen?

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.

Wat is de standaard threadveiligheidsmodus van lazy?

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 gebruik je lateinit in plaats van lazy in Android?

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

  • lateinit — modifier voor var-eigenschappen in Kotlin, die initialisatie na de constructor zonder nullable toestaat
  • lazy — eigenschapsdelegate voor val met eenmalige berekening en automatische caching van het resultaat
  • lateinit gooit UninitializedPropertyAccessException bij toegang vóór initialisatie
  • lazy is standaard thread-safe via LazyThreadSafetyMode.SYNCHRONIZED
  • lateinit is onverenigbaar met primitieve typen en nullable-eigenschappen
  • lazy kan in Android geheugenlekken veroorzaken bij het vastleggen van context in een closure
  • Kies lateinit voor veranderlijke eigenschappen en lazy voor eenmalig geïnitialiseerde val-afhankelijkheden

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.

Bespreek het project

Lees ook