Initialisation paresseuse dans Kotlin

1. Vue d'ensemble

Dans cet article, nous examinerons l'une des fonctionnalités les plus intéressantes de la syntaxe Kotlin: l'initialisation paresseuse.

Nous examinerons également le mot clé lateinit qui nous permet de tromper le compilateur et d'initialiser des champs non nuls dans le corps de la classe - plutôt que dans le constructeur.

2. Modèle d'initialisation différée en Java

Parfois, nous devons construire des objets qui ont un processus d'initialisation fastidieux. Aussi, souvent, nous ne pouvons pas être sûrs que l'objet, pour lequel nous avons payé le coût d'initialisation au début de notre programme, sera utilisé dans notre programme.

Le concept d '«initialisation paresseuse» a été conçu pour empêcher l'initialisation inutile des objets . En Java, créer un objet de manière paresseuse et sans fil n'est pas une chose facile à faire. Les modèles comme Singleton ont des failles importantes dans le multithreading, les tests, etc. - et ils sont maintenant largement connus comme des anti-modèles à éviter.

Alternativement, nous pouvons tirer parti de l'initialisation statique de l'objet interne en Java pour atteindre la paresse:

public class ClassWithHeavyInitialization { private ClassWithHeavyInitialization() { } private static class LazyHolder { public static final ClassWithHeavyInitialization INSTANCE = new ClassWithHeavyInitialization(); } public static ClassWithHeavyInitialization getInstance() { return LazyHolder.INSTANCE; } }

Remarquez comment, uniquement lorsque nous appellerons la méthode getInstance () sur ClassWithHeavyInitialization , la classe statique LazyHolder sera chargée et la nouvelle instance de ClassWithHeavyInitialization sera créée. Ensuite, l'instance sera affectée à la référence INSTANCE finale statique .

Nous pouvons tester que getInstance () renvoie la même instance à chaque fois qu'elle est appelée:

@Test public void giveHeavyClass_whenInitLazy_thenShouldReturnInstanceOnFirstCall() { // when ClassWithHeavyInitialization classWithHeavyInitialization = ClassWithHeavyInitialization.getInstance(); ClassWithHeavyInitialization classWithHeavyInitialization2 = ClassWithHeavyInitialization.getInstance(); // then assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2); }

C'est techniquement correct mais bien sûr un peu trop compliqué pour un concept aussi simple .

3. Initialisation paresseuse dans Kotlin

Nous pouvons voir que l'utilisation du modèle d'initialisation paresseuse en Java est assez fastidieuse. Nous devons écrire beaucoup de code standard pour atteindre notre objectif. Heureusement, le langage Kotlin a un support intégré pour l'initialisation paresseuse .

Pour créer un objet qui sera initialisé au premier accès, nous pouvons utiliser la méthode lazy :

@Test fun givenLazyValue_whenGetIt_thenShouldInitializeItOnlyOnce() { // given val numberOfInitializations: AtomicInteger = AtomicInteger() val lazyValue: ClassWithHeavyInitialization by lazy { numberOfInitializations.incrementAndGet() ClassWithHeavyInitialization() } // when println(lazyValue) println(lazyValue) // then assertEquals(numberOfInitializations.get(), 1) }

Comme nous pouvons le voir, le lambda passé à la fonction paresseuse n'a été exécuté qu'une seule fois.

Lorsque nous accédons à la lazyValue pour la première fois, une initialisation réelle s'est produite et l'instance renvoyée de la classe ClassWithHeavyInitialization a été affectée à la référence lazyValue . Un accès ultérieur à lazyValue a renvoyé l'objet précédemment initialisé.

Nous pouvons passer le LazyThreadSafetyMode comme argument à la fonction paresseuse . Le mode de publication par défaut est SYNCHRONIZED , ce qui signifie qu'un seul thread peut initialiser l'objet donné.

Nous pouvons passer une PUBLICATION comme mode - ce qui fera que chaque thread pourra initialiser une propriété donnée. L'objet affecté à la référence sera la première valeur renvoyée - donc le premier thread l'emporte.

Jetons un coup d'œil à ce scénario:

@Test fun whenGetItUsingPublication_thenCouldInitializeItMoreThanOnce() { // given val numberOfInitializations: AtomicInteger = AtomicInteger() val lazyValue: ClassWithHeavyInitialization by lazy(LazyThreadSafetyMode.PUBLICATION) { numberOfInitializations.incrementAndGet() ClassWithHeavyInitialization() } val executorService = Executors.newFixedThreadPool(2) val countDownLatch = CountDownLatch(1) // when executorService.submit { countDownLatch.await(); println(lazyValue) } executorService.submit { countDownLatch.await(); println(lazyValue) } countDownLatch.countDown() // then executorService.awaitTermination(1, TimeUnit.SECONDS) executorService.shutdown() assertEquals(numberOfInitializations.get(), 2) }

Nous pouvons voir que le démarrage de deux threads en même temps entraîne deux fois l'initialisation de ClassWithHeavyInitialization .

Il existe également un troisième mode - AUCUN - mais il ne doit pas être utilisé dans l'environnement multithread car son comportement n'est pas défini.

4. Lateinit de Kotlin

Dans Kotlin, chaque propriété de classe non nullable déclarée dans la classe doit être initialisée soit dans le constructeur, soit dans le cadre de la déclaration de variable. Si nous ne parvenons pas à le faire, le compilateur Kotlin se plaindra avec un message d'erreur:

Kotlin: Property must be initialized or be abstract

Cela signifie essentiellement que nous devons soit initialiser la variable, soit la marquer comme abstraite .

D'autre part, il existe certains cas dans lesquels la variable peut être affectée dynamiquement par exemple par injection de dépendances.

Pour différer l'initialisation de la variable, on peut spécifier qu'un champ est lateinit . Nous informons le compilateur que cette variable sera affectée plus tard et nous libérons le compilateur de la responsabilité de s'assurer que cette variable est initialisée:

lateinit var a: String @Test fun givenLateInitProperty_whenAccessItAfterInit_thenPass() { // when a = "it" println(a) // then not throw }

Si nous oublions d'initialiser la propriété lateinit , nous obtiendrons une exception UninitializedPropertyAccessException :

@Test(expected = UninitializedPropertyAccessException::class) fun givenLateInitProperty_whenAccessItWithoutInit_thenThrow() { // when println(a) }

Il est à noter que nous ne pouvons utiliser que des variables lateinit avec des types de données non primitifs. Par conséquent, il n'est pas possible d'écrire quelque chose comme ceci:

lateinit var value: Int

Et si nous le faisons, nous aurions une erreur de compilation:

Kotlin: 'lateinit' modifier is not allowed on properties of primitive types

5. Conclusion

Dans ce tutoriel rapide, nous avons examiné l'initialisation tardive des objets.

Tout d'abord, nous avons vu comment créer une initialisation paresseuse thread-safe en Java. Nous avons vu que c'est très encombrant et qu'il a besoin de beaucoup de code standard.

Ensuite, nous avons exploré le mot clé paresseux de Kotlin utilisé pour l'initialisation paresseuse des propriétés. En fin de compte, nous avons vu comment différer l'attribution de variables à l'aide du mot-clé lateinit .

L'implémentation de tous ces exemples et extraits de code peut être trouvée sur GitHub.