1. Información general
En este artículo, veremos una de las características más interesantes de la sintaxis de Kotlin: la inicialización diferida.
También veremos la palabra clave lateinit que nos permite engañar al compilador e inicializar campos no nulos en el cuerpo de la clase, en lugar de en el constructor.
2. Patrón de inicialización diferida en Java
A veces necesitamos construir objetos que tienen un proceso de inicialización engorroso. Además, a menudo no podemos estar seguros de que el objeto, por el que pagamos el costo de inicialización al comienzo de nuestro programa, se utilizará en nuestro programa.
El concepto de "inicialización diferida" se diseñó para evitar la inicialización innecesaria de objetos . En Java, crear un objeto de forma perezosa y segura para subprocesos no es algo fácil de hacer. Los patrones como Singleton tienen fallas significativas en multiprocesos, pruebas, etc., y ahora son ampliamente conocidos como anti-patrones que deben evitarse.
Alternativamente, podemos aprovechar la inicialización estática del objeto interno en Java para lograr la pereza:
public class ClassWithHeavyInitialization { private ClassWithHeavyInitialization() { } private static class LazyHolder { public static final ClassWithHeavyInitialization INSTANCE = new ClassWithHeavyInitialization(); } public static ClassWithHeavyInitialization getInstance() { return LazyHolder.INSTANCE; } }
Observe cómo, solo cuando llamemos al método getInstance () en ClassWithHeavyInitialization , se cargará la clase estática LazyHolder y se creará la nueva instancia de ClassWithHeavyInitialization . A continuación, la instancia se asignará a la referencia INSTANCE final estática .
Podemos probar que getInstance () devuelve la misma instancia cada vez que se llama:
@Test public void giveHeavyClass_whenInitLazy_thenShouldReturnInstanceOnFirstCall() { // when ClassWithHeavyInitialization classWithHeavyInitialization = ClassWithHeavyInitialization.getInstance(); ClassWithHeavyInitialization classWithHeavyInitialization2 = ClassWithHeavyInitialization.getInstance(); // then assertTrue(classWithHeavyInitialization == classWithHeavyInitialization2); }
Eso está técnicamente bien, pero por supuesto es un poco complicado para un concepto tan simple .
3. Inicialización diferida en Kotlin
Podemos ver que usar el patrón de inicialización diferida en Java es bastante engorroso. Necesitamos escribir mucho código repetitivo para lograr nuestro objetivo. Afortunadamente, el lenguaje Kotlin tiene soporte incorporado para inicialización diferida .
Para crear un objeto que se inicializará en el primer acceso a él, podemos usar el método 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) }
Como podemos ver, la lambda pasada a la función lazy se ejecutó solo una vez.
Cuando accedemos a lazyValue por primera vez, se produjo una inicialización real y la instancia devuelta de la clase ClassWithHeavyInitialization se asignó a la referencia lazyValue . El acceso posterior a lazyValue devolvió el objeto inicializado previamente.
Podemos pasar LazyThreadSafetyMode como un argumento a la función lazy . El modo de publicación predeterminado es SINCRONIZADO , lo que significa que solo un hilo puede inicializar el objeto dado.
Podemos pasar una PUBLICACIÓN como modo, lo que provocará que cada hilo pueda inicializar una propiedad determinada. El objeto asignado a la referencia será el primer valor devuelto, por lo que gana el primer hilo.
Echemos un vistazo a ese escenario:
@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 dos subprocesos al mismo tiempo hace que la inicialización de ClassWithHeavyInitialization suceda dos veces.
También hay un tercer modo, NINGUNO , pero no debe usarse en el entorno multiproceso ya que su comportamiento no está definido.
4. La última hora de Kotlin
En Kotlin, cada propiedad de clase que no acepta valores NULL que se declara en la clase debe inicializarse en el constructor o como parte de la declaración de la variable. Si no lo hacemos, el compilador de Kotlin se quejará con un mensaje de error:
Kotlin: Property must be initialized or be abstract
Esto básicamente significa que debemos inicializar la variable o marcarla como abstracta .
Por otro lado, hay algunos casos en los que la variable se puede asignar dinámicamente mediante, por ejemplo, inyección de dependencia.
Para diferir la inicialización de la variable, podemos especificar que un campo es tardío . Estamos informando al compilador que esta variable will se asignará más tarde y estamos liberando al compilador de la responsabilidad de asegurarse de que esta variable se inicialice:
lateinit var a: String @Test fun givenLateInitProperty_whenAccessItAfterInit_thenPass() { // when a = "it" println(a) // then not throw }
Si olvidamos inicializar la propiedad lateinit , obtendremos una UninitializedPropertyAccessException :
@Test(expected = UninitializedPropertyAccessException::class) fun givenLateInitProperty_whenAccessItWithoutInit_thenThrow() { // when println(a) }
Vale la pena mencionar que solo podemos usar variables lateinit con tipos de datos no primitivos. Por lo tanto, no es posible escribir algo como esto:
lateinit var value: Int
Y si lo hacemos, obtendríamos un error de compilación:
Kotlin: 'lateinit' modifier is not allowed on properties of primitive types
5. Conclusión
En este tutorial rápido, analizamos la inicialización diferida de objetos.
En primer lugar, vimos cómo crear una inicialización perezosa segura para subprocesos en Java. Vimos que es muy engorroso y necesita mucho código repetitivo.
A continuación, profundizamos en la palabra clave lazy de Kotlin que se utiliza para la inicialización diferida de propiedades. Al final, vimos cómo diferir la asignación de variables usando la palabra clave lateinit .
La implementación de todos estos ejemplos y fragmentos de código se puede encontrar en GitHub.