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
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).
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 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.
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.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
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 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.
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.
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.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
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é.
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ère | lateinit | lazy |
|---|---|---|
| Type de propriété | var uniquement | val uniquement |
| Nullable | non autorisé | autorisé |
| Types primitifs | non autorisés | autorisés |
| Sécurité des threads | non garantie | SYNCHRONIZED par défaut |
| Vérification d’état | ::x.isInitialized | non nécessaire |
| Exception en cas d’erreur | UninitializedPropertyAccessException | erreur dans le bloc init |
| Mise en cache | non applicable | calcul unique |
| Android Binding | View Binding, Data Binding | non utilisé |
| Frameworks DI | Dagger, Hilt, Koin | injection 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.
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.
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
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.
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.
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.
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.
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é
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.
Lisez aussi