La inicialización diferida (lazy initialization) es un mecanismo en Kotlin mediante el cual una propiedad de un objeto se inicializa no en el momento de su creación, sino en el primer acceso a ella. Según JetBrains, 2024, lateinit y lazy son dos herramientas integradas para implementar esta estrategia. Ambas resuelven el problema de la inicialización diferida, pero difieren fundamentalmente en su mecanismo de funcionamiento y ámbito de aplicación.
Puntos clave
La inicialización diferida es un patrón en el que una propiedad de clase recibe su valor no en el momento de la construcción del objeto, sino más tarde, bajo demanda. En Kotlin, este patrón se implementa mediante dos formas fundamentalmente diferentes: el modificador lateinit y el delegado lazy.
Ambos mecanismos resuelven un problema común: una propiedad debe existir en la clase, pero su valor se desconoce en el momento de la creación del objeto, o su cálculo consume demasiados recursos como para realizarlo innecesariamente. Según Google I/O 2023, hasta el 40% de las propiedades en una aplicación típica de Android se pueden optimizar mediante la inicialización diferida, reduciendo el tiempo de inicio entre un 15 y un 25%.
La elección entre lateinit y lazy viene determinada por tres factores: la mutabilidad de la propiedad (var o val), su ciclo de vida (asignación única o múltiple) y los requisitos de seguridad para subprocesos (acceso de un solo subproceso o de múltiples subprocesos).
El primer y más común escenario es la inyección de dependencias. El framework (Dagger, Hilt, Koin) inyecta las dependencias después de la creación del objeto, por lo que la propiedad no puede inicializarse en el constructor. Sin lateinit, todas las dependencias tendrían que declararse como nullable y comprobarse en cada uso.
El segundo escenario son los recursos pesados: bases de datos, clientes de red, gestores de archivos. Su creación requiere tiempo y memoria, por lo que solo deben inicializarse cuando se usen realmente. lazy es ideal para estos casos, ya que garantiza una única creación.
La tercera situación son los componentes de Android (Activity, Fragment, ViewModel), cuyo ciclo de vida es gestionado por el sistema operativo. Las propiedades que dependen de onCreate, onViewCreated o el bloque init de ViewModel no pueden inicializarse en el constructor.
lateinit es un modificador para propiedades var que permite al compilador de Kotlin diferir la inicialización. El compilador no exige la asignación de un valor en el constructor, pero genera una comprobación en tiempo de ejecución en cada acceso: si la propiedad no está inicializada, lanza UninitializedPropertyAccessException.
class MainActivity {
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
Limitaciones de lateinit: la propiedad debe declararse como var (no val), no anulable y no de tipo primitivo (Int, Double, Boolean, etc.). La razón es que los tipos primitivos se compilan a primitivos de JVM, que no tienen un estado “no inicializado”. Para las propiedades anulables, la inicialización diferida es innecesaria: null ya significa ausencia de valor.
Para comprobar el estado de una propiedad lateinit, se utiliza la referencia incorporada mediante el operador ::: ::propertyName.isInitialized. Esta es la única forma segura de comprobar si una propiedad está inicializada sin arriesgarse a una excepción. La comprobación solo está disponible desde la misma clase o una clase interna, no desde código externo.
class LoginFragment {
lateinit var binding: FragmentLoginBinding
fun isReady(): Boolean {
return ::binding.isInitialized
}
}
lateinit no añade sobrecarga después de la inicialización: una vez asignado el valor, el acceso a la propiedad es idéntico al acceso directo al campo. El único costo es la comprobación de inicialización en cada lectura antes de la asignación. Después de la inicialización, el compilador JIT optimiza la comprobación.
Un detalle importante: las propiedades lateinit no se pueden utilizar en clases inline y no son compatibles con propiedades que tienen getters/setters personalizados. Si una propiedad requiere acceso calculado, use lazy en lugar de lateinit.
lazy es un delegado de propiedad integrado en la biblioteca estándar de Kotlin. Calcula el valor en el primer acceso a la propiedad y almacena en caché el resultado para todas las llamadas posteriores. A diferencia de lateinit, lazy solo funciona con val, lo que hace que la propiedad sea inmutable después de la inicialización.
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 acepta un parámetro opcional LazyThreadSafetyMode que controla el mecanismo de seguridad para subprocesos. El valor predeterminado es SYNCHRONIZED, que utiliza doble verificación con bloqueo, garantizando una única inicialización incluso bajo acceso concurrente desde varios subprocesos.
El modo PUBLICATION permite la inicialización en paralelo: varios subprocesos pueden ejecutar simultáneamente el bloque de inicialización, pero el resultado solo se acepta del primero en completarse. Es más rápido que SYNCHRONIZED bajo alta contención, pero aumenta el consumo de recursos.
El modo NONE desactiva completamente la sincronización. Úselo solo para propiedades cuyo acceso esté garantizado desde un único subproceso. En este modo, lazy opera con una sobrecarga mínima, casi como una asignación directa.
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
Config.loadFromFile("config.json")
}
lazy es la elección correcta para dependencias que se inicializan una sola vez: repositorios, clientes de red, cachés, bases de datos. La semántica de val protege contra la sobrescritura accidental, y la seguridad para subprocesos por defecto hace que el código sea seguro en entornos de múltiples subprocesos. lazy también funciona correctamente con tipos primitivos, algo imposible con lateinit.
En Android, lazy se utiliza a menudo para inicializar dependencias de ViewModel mediante by viewModels() o para crear clientes Retrofit. Sin embargo, hay que tener cuidado: si un bloque lazy captura una referencia a una Activity o Fragment, puede provocar una fuga de memoria, ya que el delegado conserva el cierre durante toda la vida de la propiedad.
La elección entre lateinit y lazy no es una cuestión de preferencia, sino una decisión arquitectónica determinada por la naturaleza de la propiedad. Cada mecanismo resuelve su propia tarea y sus áreas de aplicación solo se superponen parcialmente.
| Criterio | lateinit | lazy |
|---|---|---|
| Tipo de propiedad | solo var | solo val |
| Nullable | no permitido | permitido |
| Tipos primitivos | no permitidos | permitidos |
| Seguridad para subprocesos | no garantizada | SYNCHRONIZED por defecto |
| Comprobación de estado | ::x.isInitialized | no requerida |
| Excepción en error | UninitializedPropertyAccessException | error en bloque de init |
| Caché | no aplica | cálculo único |
| Android Binding | View Binding, Data Binding | no se usa |
| Frameworks DI | Dagger, Hilt, Koin | inyección manual |
Use lateinit cuando una propiedad deba cambiar después de la inicialización o su creación sea gestionada por código externo. Un ejemplo típico es View Binding en Android Activity: el binding se crea en onCreate pero sigue siendo var porque el framework no admite val para este escenario.
Use lazy cuando una propiedad se inicialice una sola vez, su cálculo sea costoso y el valor no cambie durante la vida del objeto. Un ejemplo clásico es la creación diferida de un cliente Retrofit o una base de datos Room en el primer acceso al repositorio.
Ambos mecanismos se pueden usar simultáneamente dentro de una misma clase. Por ejemplo, lateinit para View Binding y lazy para un repositorio. Esta es una práctica normal que refleja diferentes requisitos para diferentes propiedades. La clave es no confundir la semántica: no use lateinit donde se necesita val, y no use lazy para propiedades que deben reasignarse.
El error más común con lateinit es acceder a la propiedad antes de que se inicialice. Esto provoca UninitializedPropertyAccessException, que no se detecta en tiempo de compilación porque Kotlin confía en que el desarrollador garantice el orden correcto de inicialización. La solución es comprobar siempre el estado mediante ::property.isInitialized antes del acceso en situaciones ambiguas.
El segundo problema común es usar lateinit para propiedades que semánticamente son val. Si el valor se establece una vez y nunca cambia, lazy es la opción más correcta. Hace que la propiedad sea inmutable, evita la sobrescritura accidental y añade seguridad para subprocesos de forma gratuita.
El tercer error es lazy con efectos secundarios. El bloque de inicialización de lazy no debe modificar el estado externo ni depender del orden de inicialización de otras propiedades lazy, ya que la secuencia de cálculo depende del primer acceso y puede no ser obvia. Si las propiedades lazy se referencian entre sí, se produce una dependencia circular y StackOverflowError.
El cuarto problema son las fugas de memoria mediante lazy en Android. Si un bloque lazy captura una referencia a una Activity o Fragment, el delegado conserva el cierre y el recolector de basura no puede liberar el componente incluso después de su destrucción. La solución es usar lazy solo con objetos de corta vida o pasar el contexto de Application en lugar de Activity.
El quinto error típico es intentar aplicar lateinit a tipos primitivos. El compilador de Kotlin lo bloquea a nivel de sintaxis, pero los desarrolladores intentan eludir la limitación mediante envoltorios anulables. Esto provoca comprobaciones null innecesarias y anula por completo los beneficios de la inicialización diferida.
Preguntas frecuentes
lateinit es un modificador para propiedades var, que permite la inicialización después del constructor. lazy es un delegado para propiedades val, que calcula el valor en el primer acceso y lo almacena en caché. lateinit no admite tipos primitivos ni nullable, mientras que lazy es seguro para subprocesos por defecto.
Sí, mediante la referencia incorporada a la propiedad: ::propertyName.isInitialized. El método devuelve true si la propiedad ya se ha inicializado. Esta es la única forma segura de evitar UninitializedPropertyAccessException al trabajar con campos lateinit.
Los tipos primitivos — Int, Double, Boolean y otros — se compilan a primitivos de JVM (int, double, boolean), que no tienen un estado “no inicializado”. lateinit utiliza null como indicador, y los primitivos no pueden ser null, por lo que el mecanismo es físicamente imposible para estos tipos.
El valor predeterminado es LazyThreadSafetyMode.SYNCHRONIZED — doble verificación con bloqueo, que garantiza una única inicialización bajo acceso concurrente desde varios subprocesos. Para escenarios de un solo subproceso, use NONE; para alta contención, use PUBLICATION.
Cuando la propiedad deba cambiar después de la inicialización o su creación sea gestionada por el framework. Un ejemplo típico es View Binding en Android Activity: el binding se crea en onCreate y debe ser var. Para dependencias val que se inicializan una sola vez, use lazy.
Resumen
Desarrollaremos una aplicación móvil llave en mano
IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.
Lea también