lateinit / lazy : l’essence de l’initialisation différée et ses mécanismes en Kotlin

Auteur : IT Sectr Publié le : 2026-06-23 Temps de lecture : 8 min

L’initialisation différée (lazy initialization) est un mécanisme en Kotlin dans lequel une propriété d’objet est initialisée non pas au moment de la création, mais lors du premier accès. Selon JetBrains, 2024, lateinit et lazy sont deux outils intégrés pour implémenter cette stratégie. Les deux résolvent le problème de l’initialisation différée, mais diffèrent fondamentalement par leur mécanisme de fonctionnement et leur domaine d’application.

Points clés

  • lateinit — un modificateur pour les propriétés var, permettant l’initialisation après la création de l’objet
  • lazy — un délégué pour les propriétés val, initialisant la valeur au premier accès
  • lateinit nécessite var et ne prend pas en charge les types primitifs JVM
  • lazy est thread-safe par défaut et met en cache le résultat calculé
  • lateinit lève UninitializedPropertyAccessException en cas d’accès prématuré

Qu’est-ce que l’initialisation différée en Kotlin ?

L’initialisation différée est un modèle dans lequel une propriété de classe reçoit sa valeur non pas au moment de la construction de l’objet, mais plus tard, à la demande. En Kotlin, ce modèle est implémenté de deux manières fondamentalement différentes : le modificateur lateinit et le délégué lazy.

Les deux mécanismes résolvent un problème commun — une propriété doit exister dans la classe, mais sa valeur est soit inconnue au moment de la création de l’objet, soit son calcul est trop gourmand en ressources pour être effectué inutilement. Selon Google I/O 2023, jusqu’à 40 % des propriétés dans une application Android typique peuvent être optimisées grâce à l’initialisation différée, ce qui réduit le temps de démarrage de 15 à 25 %.

Le choix entre lateinit et lazy est déterminé par trois facteurs : la mutabilité de la propriété (var ou val), son cycle de vie (affectation unique ou multiple) et les exigences de sécurité des threads (accès mono-thread ou multi-thread).

Quand l’initialisation différée est utilisée

Le premier scénario, le plus courant, est l’injection de dépendances. Le framework (Dagger, Hilt, Koin) injecte les dépendances après la création de l’objet, la propriété ne peut donc pas être initialisée dans le constructeur. Sans lateinit, toutes les dépendances devraient être déclarées nullable et vérifiées à chaque utilisation.

Le deuxième scénario concerne les ressources lourdes : bases de données, clients réseau, gestionnaires de fichiers. Leur création nécessite du temps et de la mémoire, elles ne doivent donc être initialisées qu’en cas d’utilisation réelle. lazy est idéal pour ces cas, garantissant une création unique.

La troisième situation concerne les composants Android (Activity, Fragment, ViewModel) dont le cycle de vie est géré par le système d’exploitation. Les propriétés qui dépendent de onCreate, onViewCreated ou du bloc init du ViewModel ne peuvent pas être initialisées dans le constructeur.

lateinit : mécanisme et limites

lateinit est un modificateur pour les propriétés var qui permet au compilateur Kotlin de différer l’initialisation. Le compilateur n’exige pas d’affectation de valeur dans le constructeur, mais génère une vérification à l’exécution à chaque accès : si la propriété n’est pas initialisée, il lève UninitializedPropertyAccessException.

kotlin
class MainActivity {
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

Limites de lateinit : la propriété doit être déclarée comme var (pas val), non nullable et non de type primitif (Int, Double, Boolean, etc.). La raison est que les types primitifs sont compilés en primitifs JVM, qui n’ont pas d’état « non initialisé ». Pour les propriétés nullables, l’initialisation différée est inutile : null signifie déjà l’absence de valeur.

Pour vérifier l’état d’une propriété lateinit, utilisez la référence intégrée via l’opérateur :: : ::propertyName.isInitialized. C’est le seul moyen sûr de vérifier si une propriété est initialisée sans risquer une exception. La vérification n’est disponible qu’à partir de la même classe ou d’une classe interne, pas de code externe.

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

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

Performances de lateinit

lateinit n’ajoute aucun surcoût après l’initialisation : une fois la valeur affectée, l’accès à la propriété est identique à l’accès direct au champ. Le seul coût est la vérification d’initialisation à chaque lecture avant l’affectation. Après l’initialisation, le compilateur JIT optimise la vérification.

Une remarque importante : les propriétés lateinit ne peuvent pas être utilisées dans les classes inline et ne sont pas prises en charge pour les propriétés avec des getters/setters personnalisés. Si une propriété nécessite un accès calculé, utilisez lazy au lieu de lateinit.

lazy : mécanisme et avantages

lazy est un délégué de propriété intégré à la bibliothèque standard de Kotlin. Il calcule la valeur au premier accès à la propriété et met en cache le résultat pour tous les appels ultérieurs. Contrairement à lateinit, lazy ne fonctionne qu’avec val, rendant la propriété immuable après l’initialisation.

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 accepte un paramètre facultatif LazyThreadSafetyMode qui contrôle le mécanisme de sécurité des threads. La valeur par défaut est SYNCHRONIZED — double vérification avec verrouillage, garantissant une initialisation unique même en cas d’accès simultané à partir de plusieurs threads.

Modes de sécurité des threads de lazy

Le mode PUBLICATION permet l’initialisation parallèle : plusieurs threads peuvent exécuter simultanément le bloc d’initialisation, mais le résultat n’est accepté que du premier à se terminer. C’est plus rapide que SYNCHRONIZED en cas de forte contention, mais augmente la consommation de ressources.

Le mode NONE désactive complètement la synchronisation. Utilisez-le uniquement pour les propriétés dont l’accès est garanti à partir d’un seul thread. Dans ce mode, lazy fonctionne avec une surcharge minimale — presque comme une affectation directe.

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

Quand lazy est préférable

lazy est le bon choix pour les dépendances initialisées une seule fois : référentiels, clients réseau, caches, bases de données. La sémantique de val protège contre l’écrasement accidentel, et la sécurité des threads par défaut rend le code sûr dans les environnements multi-threads. lazy fonctionne également correctement avec les types primitifs, ce qui est impossible avec lateinit.

Dans Android, lazy est souvent utilisé pour initialiser les dépendances ViewModel via by viewModels() ou pour créer des clients Retrofit. Cependant, soyez prudent : si un bloc lazy capture une référence à une Activity ou un Fragment, cela peut entraîner une fuite mémoire, car le délégué conserve la fermeture pendant toute la durée de vie de la propriété.

lateinit vs lazy : comparaison des approches

Le choix entre lateinit et lazy n’est pas une question de préférence, mais une décision architecturale déterminée par la nature de la propriété. Chaque mécanisme résout sa propre tâche et leurs domaines d’application ne se chevauchent que partiellement.

Critèrelateinitlazy
Type de propriétévar uniquementval uniquement
Nullablenon autoriséautorisé
Types primitifsnon autorisésautorisés
Sécurité des threadsnon garantieSYNCHRONIZED par défaut
Vérification d’état::x.isInitializednon nécessaire
Exception en cas d’erreurUninitializedPropertyAccessExceptionerreur dans le bloc init
Mise en cachenon applicablecalcul unique
Android BindingView Binding, Data Bindingnon utilisé
Frameworks DIDagger, Hilt, Koininjection manuelle

Utilisez lateinit lorsqu’une propriété doit changer après l’initialisation ou que sa création est gérée par du code externe. Un exemple typique est View Binding dans Android Activity : le binding est créé dans onCreate mais reste var car le framework ne prend pas en charge val pour ce scénario.

Utilisez lazy lorsqu’une propriété est initialisée une fois, que son calcul est coûteux et que la valeur ne change pas pendant la durée de vie de l’objet. Un exemple classique est la création paresseuse d’un client Retrofit ou d’une base de données Room lors du premier accès au référentiel.

Combinaison de lateinit et lazy

Les deux mécanismes peuvent être utilisés simultanément dans une même classe. Par exemple, lateinit pour View Binding et lazy pour un référentiel. C’est une pratique normale qui reflète des exigences différentes pour des propriétés différentes. L’essentiel est de ne pas confondre la sémantique : n’utilisez pas lateinit là où val est nécessaire, et n’utilisez pas lazy pour les propriétés qui doivent être réaffectées.

Erreurs courantes lors de l’utilisation de lateinit et lazy

L’erreur la plus fréquente avec lateinit est d’accéder à la propriété avant qu’elle ne soit initialisée. Cela conduit à UninitializedPropertyAccessException, qui n’est pas interceptée à la compilation car Kotlin fait confiance au développeur pour garantir l’ordre d’initialisation correct. La solution est de toujours vérifier l’état via ::property.isInitialized avant l’accès dans les situations ambiguës.

Le deuxième problème courant est d’utiliser lateinit pour des propriétés sémantiquement val. Si la valeur est définie une fois et ne change jamais, lazy est le choix le plus correct. Il rend la propriété immuable, évite l’écrasement accidentel et ajoute la sécurité des threads gratuitement.

La troisième erreur est lazy avec des effets secondaires. Le bloc d’initialisation lazy ne doit pas modifier l’état externe ni dépendre de l’ordre d’initialisation d’autres propriétés lazy, car la séquence de calcul dépend du premier accès et peut ne pas évidente. Si les propriétés lazy se référencent les unes aux autres, cela entraîne une dépendance circulaire et StackOverflowError.

Le quatrième problème concerne les fuites mémoire via lazy dans Android. Si un bloc lazy capture une référence à une Activity ou un Fragment, le délégué conserve la fermeture et le ramasse-miettes ne peut pas libérer le composant même après sa destruction. La solution est d’utiliser lazy uniquement avec des objets à courte durée de vie ou de passer le contexte Application plutôt que l’Activity.

La cinquième erreur typique est de tenter d’appliquer lateinit aux types primitifs. Le compilateur Kotlin bloque cela au niveau syntaxique, mais les développeurs tentent de contourner la limitation via des wrappers nullables. Cela entraîne des vérifications null inutiles et annule complètement les avantages de l’initialisation différée.

Questions fréquentes

Quelle est la différence entre lateinit et lazy en Kotlin ?

lateinit est un modificateur pour les propriétés var, permettant l’initialisation après le constructeur. lazy est un délégué pour les propriétés val, qui calcule la valeur au premier accès et la met en cache. lateinit ne prend pas en charge les types primitifs et nullable, tandis que lazy est thread-safe par défaut.

Puis-je vérifier si une propriété lateinit est initialisée ?

Oui, via la référence intégrée à la propriété : ::propertyName.isInitialized. La méthode renvoie true si la propriété a déjà été initialisée. C’est le seul moyen sûr d’éviter UninitializedPropertyAccessException lors du travail avec les champs lateinit.

Pourquoi lateinit ne peut-il pas être utilisé avec les types primitifs ?

Les types primitifs — Int, Double, Boolean et autres — sont compilés en primitifs JVM (int, double, boolean), qui n’ont pas d’état « non initialisé ». lateinit utilise null comme indicateur, et les primitifs ne peuvent pas être null, donc le mécanisme est physiquement impossible pour ces types.

Quel est le mode de sécurité des threads par défaut de lazy ?

La valeur par défaut est LazyThreadSafetyMode.SYNCHRONIZED — double vérification avec verrouillage, garantissant une initialisation unique sous accès simultané à partir de plusieurs threads. Pour les scénarios mono-thread, utilisez NONE ; pour une forte contention, utilisez PUBLICATION.

Quand dois-je utiliser lateinit au lieu de lazy dans Android ?

Lorsque la propriété doit changer après l’initialisation ou que sa création est gérée par le framework. Un exemple typique est View Binding dans Android Activity : le binding est créé dans onCreate et doit être var. Pour les dépendances val initialisées une fois, utilisez lazy.

Résumé

  • lateinit — un modificateur pour les propriétés var de Kotlin, permettant l’initialisation après le constructeur sans nullable
  • lazy — un délégué de propriété pour val avec calcul unique et mise en cache automatique du résultat
  • lateinit lève UninitializedPropertyAccessException en cas d’accès avant l’initialisation
  • lazy est thread-safe par défaut via LazyThreadSafetyMode.SYNCHRONIZED
  • lateinit est incompatible avec les types primitifs et les propriétés nullables
  • lazy peut entraîner des fuites mémoire dans Android lors de la capture du contexte dans une fermeture
  • Choisissez lateinit pour les propriétés mutables et lazy pour les dépendances val initialisées une fois

Nous développerons une application mobile clé en main

IT Sectr crée des applications iOS et Android pour les startups et les entreprises depuis 2017. Nous vous conseillerons et vous proposerons la meilleure solution.

Discuter du projet

Lisez aussi