Initialisation paresseuse dans Kotlin

Initialisation paresseuse à Kotlin

1. Vue d'ensemble

Dans cet article, nous allons examiner 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 avons besoin de construire des objets qui ont un processus d’initialisation fastidieux. De plus, nous ne pouvons souvent pas être sûrs que cet objet, pour lequel nous avons payé le coût de l'initialisation au début de notre programme, sera utilisé dans notre programme.

Le concept de‘lazy initialization' was designed to prevent unnecessary initialization of objects. En Java, créer un objet de manière paresseuse et sécurisée pour les threads n’est pas une chose facile à faire. Les modèles tels queSingleton présentent des défauts significatifs dans le multithreading, les tests, etc. - et ils sont maintenant largement connus comme des anti-modèles à éviter.

Alternativement, nous pouvons utiliser l’initialisation statique d’un objet interne en Java pour obtenir de 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, seulement lorsque nous appellerons la méthodegetInstance() surClassWithHeavyInitialization, la classe statiqueLazyHolder sera chargée et la nouvelle instance desClassWithHeavyInitialization sera créée. Ensuite, l'instance sera affectée à la référencestaticfinalINSTANCE.

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

@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ûra little bit too complicated for such a simple concept.

3. Initialisation paresseuse à Kotlin

Nous pouvons voir que l’utilisation d’un modèle d’initialisation paresseux en Java est assez lourde. Nous devons écrire beaucoup de code standard pour atteindre notre objectif. Luckily, the Kotlin language has built-in support for lazy initialization.

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

@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 fonctionlazy n'a été exécuté qu'une seule fois.

Lorsque nous accédons auxlazyValue pour la première fois - une initialisation réelle s’est produite et l’instance renvoyée de la classeClassWithHeavyInitialization a été affectée à la référencelazyValue. L'accès ultérieur auxlazyValue a renvoyé l'objet précédemment initialisé.

Nous pouvons passer leLazyThreadSafetyMode comme argument à la fonctionlazy. Le mode de publication par défaut estSYNCHRONIZED, ce qui signifie que seul un seul thread peut initialiser l'objet donné.

Nous pouvons passer unPUBLICATION comme mode - ce qui fera que chaque thread peut initialiser une propriété donnée. L'objet assigné à la référence sera la première valeur renvoyée - ainsi le premier thread gagne.

Jetons un œ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 provoque l'initialisation desClassWithHeavyInitialization deux fois.

Il existe également un troisième mode -NONE –, 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 affectée dans le constructeur; sinon, nous obtiendrons une erreur du compilateur. Par ailleurs, dans certains cas, la variable peut être affectée dynamiquement, par exemple par injection de dépendance.

Pour différer l'initialisation de la variable, nous pouvons spécifier qu'un champ estlateinit. 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 unUninitializedPropertyAccessException:

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

5. Conclusion

Dans ce rapide tutoriel, nous avons examiné l’initialisation paresseuse d’objets.

Tout d'abord, nous avons vu comment créer une initialisation différée thread-safe en Java. Nous avons vu que c’est très lourd et qu’il faut beaucoup de code standard.

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

L'implémentation de tous ces exemples et extraits de code peut être trouvée dans leGitHub project - il s'agit d'un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.