lateinit / lazy: la esencia de la inicialización diferida y sus mecanismos en Kotlin

Autor: IT Sectr Publicado: 2026-06-23 Tiempo de lectura: 8 min

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

  • lateinit — un modificador para propiedades var, que permite la inicialización después de la creación del objeto
  • lazy — un delegado para propiedades val, que inicializa el valor en el primer acceso
  • lateinit requiere var y no admite tipos primitivos de JVM
  • lazy es seguro para subprocesos por defecto y almacena en caché el resultado calculado
  • lateinit lanza UninitializedPropertyAccessException en caso de acceso prematuro

¿Qué es la inicialización diferida en Kotlin?

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).

Cuándo se utiliza la inicialización diferida

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: mecanismo y limitaciones

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.

kotlin
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.

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

    fun isReady(): Boolean {
        return ::binding.isInitialized
    }
}

Rendimiento de lateinit

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: mecanismo y ventajas

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.

kotlin
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.

Modos de seguridad para subprocesos de lazy

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.

kotlin
val heavyConfig: Config by lazy(LazyThreadSafetyMode.NONE) {
    Config.loadFromFile("config.json")
}

Cuándo es preferible lazy

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.

lateinit vs lazy: comparación de enfoques

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.

Criteriolateinitlazy
Tipo de propiedadsolo varsolo val
Nullableno permitidopermitido
Tipos primitivosno permitidospermitidos
Seguridad para subprocesosno garantizadaSYNCHRONIZED por defecto
Comprobación de estado::x.isInitializedno requerida
Excepción en errorUninitializedPropertyAccessExceptionerror en bloque de init
Cachéno aplicacálculo único
Android BindingView Binding, Data Bindingno se usa
Frameworks DIDagger, Hilt, Koininyecció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.

Combinando lateinit y lazy

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.

Errores comunes al usar lateinit y lazy

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

¿Cuál es la diferencia entre lateinit y lazy en Kotlin?

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.

¿Se puede comprobar si una propiedad lateinit está inicializada?

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.

¿Por qué no se puede usar lateinit con tipos primitivos?

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.

¿Cuál es el modo de seguridad para subprocesos predeterminado de lazy?

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.

¿Cuándo debería usar lateinit en lugar de lazy en Android?

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

  • lateinit — un modificador para propiedades var de Kotlin, que permite la inicialización después del constructor sin nullable
  • lazy — un delegado de propiedad para val con cálculo único y almacenamiento automático en caché del resultado
  • lateinit lanza UninitializedPropertyAccessException al acceder antes de la inicialización
  • lazy es seguro para subprocesos por defecto mediante LazyThreadSafetyMode.SYNCHRONIZED
  • lateinit es incompatible con tipos primitivos y propiedades anulables
  • lazy puede provocar fugas de memoria en Android al capturar el contexto en un cierre
  • Elija lateinit para propiedades mutables y lazy para dependencias val que se inicializan una sola vez

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.

Discutir el proyecto

Lea también