Inicialização preguiçosa em Kotlin

Inicialização preguiçosa em Kotlin

1. Visão geral

Neste artigo, veremos um dos recursos mais interessantes da sintaxe Kotlin - inicialização lenta.

Também veremos a palavra-chavelateinit que nos permite enganar o compilador e inicializar campos não nulos no corpo da classe - em vez de no construtor.

2. Padrão de inicialização lenta em Java

Às vezes, precisamos construir objetos que tenham um processo de inicialização complicado. Além disso, muitas vezes não podemos ter certeza de que o objeto pelo qual pagamos o custo da inicialização no início de nosso programa será usado em nosso programa.

O conceito de‘lazy initialization' was designed to prevent unnecessary initialization of objects. Em Java, criar um objeto de maneira lenta e segura para threads não é uma tarefa fácil. Padrões comoSingleton têm falhas significativas em multithreading, testes, etc. - e agora são amplamente conhecidos como anti-padrões a serem evitados.

Como alternativa, podemos aproveitar a inicialização estática do objeto interno em Java para obter preguiça:

public class ClassWithHeavyInitialization {

    private ClassWithHeavyInitialization() {
    }

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

    public static ClassWithHeavyInitialization getInstance() {
        return LazyHolder.INSTANCE;
    }
}

Observe como, apenas quando chamarmos o métodogetInstance() emClassWithHeavyInitialization, a classeLazyHolder estática será carregada e a nova instância deClassWithHeavyInitialization será criada. Em seguida, a instância será atribuída à referênciastaticfinalINSTANCE.

Podemos testar segetInstance() está retornando a mesma instância toda vez que é chamado:

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

    // then
    assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2);
}

Tecnicamente, isso está bom, mas é claroa little bit too complicated for such a simple concept.

3. Inicialização preguiçosa em Kotlin

Podemos ver que o uso de padrão de inicialização lento em Java é bastante complicado. Precisamos escrever muito código padrão para alcançar nosso objetivo. Luckily, the Kotlin language has built-in support for lazy initialization.

Para criar um objeto que será inicializado no primeiro acesso a ele, podemos usar o métodolazy:

@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)
}

Como podemos ver, o lambda passado para a funçãolazy foi executado apenas uma vez.

Quando estamos acessandolazyValue pela primeira vez - uma inicialização real aconteceu, e a instância retornada da classeClassWithHeavyInitialization foi atribuída à referêncialazyValue. O acesso subsequente alazyValue retornou o objeto inicializado anteriormente.

Podemos passarLazyThreadSafetyMode como um argumento para a funçãolazy. O modo de publicação padrão éSYNCHRONIZED, o que significa que apenas um único encadeamento pode inicializar o objeto fornecido.

Podemos passarPUBLICATION como um modo - o que fará com que cada thread possa inicializar a propriedade dada. O objeto atribuído à referência será o primeiro valor retornado - portanto, o primeiro encadeamento vence.

Vamos dar uma olhada nesse cenário:

@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)
}

Podemos ver que iniciar dois threads ao mesmo tempo faz com que a inicialização deClassWithHeavyInitialization aconteça duas vezes.

Também há um terceiro modo -NONE –, mas não deve ser usado no ambiente multithread, pois seu comportamento é indefinido.

4. lateinit de Kotlin

Em Kotlin, cada propriedade de classe não anulável declarada na classe precisa ser atribuída no construtor; caso contrário, obteremos um erro do compilador. Por outro lado, há alguns casos em que a variável pode ser atribuída dinamicamente por, por exemplo, injeção de dependência.

Para adiar a inicialização da variável, podemos especificar que um campo élateinit.. Estamos informando o compilador que esta variável será atribuída posteriormente e estamos liberando o compilador da responsabilidade de garantir que essa variável seja inicializada:

lateinit var a: String

@Test
fun givenLateInitProperty_whenAccessItAfterInit_thenPass() {
    // when
    a = "it"
    println(a)

    // then not throw
}

Se esquecermos de inicializar a propriedadelateinit, obteremos umUninitializedPropertyAccessException:

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

5. Conclusão

Neste tutorial rápido, vimos a inicialização lenta de objetos.

Primeiramente, vimos como criar uma inicialização lenta em thread-safe em Java. Vimos que é muito complicado e precisa de muito código padrão.

Em seguida, investigamos a palavra-chave Kotlinlazy, usada para inicialização lenta de propriedades. No final, vimos como adiar a atribuição de variáveis ​​usando a palavra-chavelateinit.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - este é um projeto Maven, portanto, deve ser fácil de importar e executar como está.