lateinit / lazy: a essência da inicialização diferida e seus mecanismos em Kotlin

Autor: IT Sectr Publicado: 2026-06-23 Tempo de leitura: 8 min

A inicialização diferida (lazy initialization) é um mecanismo em Kotlin no qual uma propriedade de um objeto é inicializada não no momento da criação, mas no primeiro acesso a ela. De acordo com JetBrains, 2024, lateinit e lazy são duas ferramentas integradas para implementar essa estratégia. Ambas resolvem o problema da inicialização diferida, mas diferem fundamentalmente no mecanismo de funcionamento e no escopo de aplicação.

Pontos principais

  • lateinit — um modificador para propriedades var, permitindo a inicialização após a criação do objeto
  • lazy — um delegado para propriedades val, inicializando o valor no primeiro acesso
  • lateinit requer var e não suporta tipos primitivos da JVM
  • lazy é seguro para threads por padrão e armazena em cache o resultado calculado
  • lateinit lança UninitializedPropertyAccessException em caso de acesso prematuro

O que é inicialização diferida em Kotlin?

Inicialização diferida é um padrão no qual uma propriedade de classe recebe seu valor não no momento da construção do objeto, mas depois, sob demanda. Em Kotlin, esse padrão é implementado de duas maneiras fundamentalmente diferentes: o modificador lateinit e o delegado lazy.

Ambos os mecanismos resolvem um problema comum — uma propriedade deve existir na classe, mas seu valor é desconhecido no momento da criação do objeto, ou seu cálculo é muito intensivo em recursos para ser executado desnecessariamente. De acordo com Google I/O 2023, até 40% das propriedades em um aplicativo Android típico podem ser otimizadas por meio da inicialização diferida, reduzindo o tempo de inicialização em 15–25%.

A escolha entre lateinit e lazy é determinada por três fatores: mutabilidade da propriedade (var ou val), seu ciclo de vida (atribuição única ou múltipla) e requisitos de segurança de thread (acesso single-thread ou multi-thread).

Quando a inicialização diferida é usada

O primeiro e mais comum cenário é a Injeção de Dependência. O framework (Dagger, Hilt, Koin) injeta dependências após a criação do objeto, portanto, a propriedade não pode ser inicializada no construtor. Sem lateinit, todas as dependências teriam que ser declaradas como nullable e verificadas a cada uso.

O segundo cenário são os recursos pesados: bancos de dados, clientes de rede, gerenciadores de arquivos. Sua criação requer tempo e memória, portanto, devem ser inicializados apenas quando realmente usados. lazy é ideal para esses casos, garantindo criação única.

A terceira situação são os componentes Android (Activity, Fragment, ViewModel), cujo ciclo de vida é gerenciado pelo sistema operacional. Propriedades que dependem de onCreate, onViewCreated ou do bloco init do ViewModel não podem ser inicializadas no construtor.

lateinit: mecanismo e limitações

lateinit é um modificador para propriedades var que permite ao compilador Kotlin adiar a inicialização. O compilador não exige uma atribuição de valor no construtor, mas gera uma verificação em tempo de execução em cada acesso: se a propriedade não estiver inicializada, lança UninitializedPropertyAccessException.

kotlin
class MainActivity {
    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
    }
}

Limitações do lateinit: a propriedade deve ser declarada como var (não val), não anulável e não de tipo primitivo (Int, Double, Boolean, etc.). O motivo é que tipos primitivos compilam para primitivos JVM, que não têm um estado “não inicializado”. Para propriedades anuláveis, a inicialização diferida é desnecessária: null já significa ausência de valor.

Para verificar o estado de uma propriedade lateinit, use a referência embutida através do operador ::: ::propertyName.isInitialized. Esta é a única maneira segura de verificar se uma propriedade foi inicializada sem arriscar uma exceção. A verificação está disponível apenas na mesma classe ou classe interna, não em código externo.

kotlin
class LoginFragment {
    lateinit var binding: FragmentLoginBinding

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

Desempenho do lateinit

lateinit não adiciona sobrecarga após a inicialização: uma vez atribuído o valor, o acesso à propriedade é idêntico ao acesso direto ao campo. O único custo é a verificação de inicialização em cada leitura antes da atribuição. Após a inicialização, o compilador JIT otimiza a verificação.

Uma observação importante: propriedades lateinit não podem ser usadas em classes inline e não são suportadas para propriedades com getters/setters personalizados. Se uma propriedade requer acesso computado, use lazy em vez de lateinit.

lazy: mecanismo e vantagens

lazy é um delegado de propriedade integrado à biblioteca padrão do Kotlin. Ele calcula o valor no primeiro acesso à propriedade e armazena em cache o resultado para todas as chamadas subsequentes. Ao contrário do lateinit, lazy funciona apenas com val, tornando a propriedade imutável após a inicialização.

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 aceita um parâmetro opcional LazyThreadSafetyMode que controla o mecanismo de segurança de thread. O padrão é SYNCHRONIZED — verificação dupla com bloqueio, garantindo inicialização única mesmo sob acesso concorrente de várias threads.

Modos de segurança de thread do lazy

O modo PUBLICATION permite inicialização paralela: várias threads podem executar o bloco de inicialização simultaneamente, mas o resultado só é aceito da primeira a concluir. É mais rápido que SYNCHRONIZED sob alta contenção, mas aumenta o consumo de recursos.

O modo NONE desativa completamente a sincronização. Use-o apenas para propriedades cujo acesso é garantido a partir de uma única thread. Neste modo, lazy opera com sobrecarga mínima — quase como uma atribuição direta.

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

Quando lazy é preferível

lazy é a escolha certa para dependências inicializadas uma vez: repositórios, clientes de rede, caches, bancos de dados. A semântica de val protege contra sobrescrita acidental, e a segurança de thread padrão torna o código seguro em ambientes multi-thread. lazy também funciona corretamente com tipos primitivos, o que é impossível com lateinit.

No Android, lazy é frequentemente usado para inicializar dependências do ViewModel via by viewModels() ou para criar clientes Retrofit. No entanto, tenha cuidado: se um bloco lazy capturar uma referência a uma Activity ou Fragment, pode causar um vazamento de memória, pois o delegado mantém o fechamento durante toda a vida da propriedade.

lateinit vs lazy: comparação de abordagens

A escolha entre lateinit e lazy não é uma questão de preferência, mas uma decisão arquitetônica determinada pela natureza da propriedade. Cada mecanismo resolve sua própria tarefa e suas áreas de aplicação se sobrepõem apenas parcialmente.

Critériolateinitlazy
Tipo de propriedadeapenas varapenas val
Anulávelnão permitidopermitido
Tipos primitivosnão permitidospermitidos
Segurança de threadnão garantidaSYNCHRONIZED por padrão
Verificação de estado::x.isInitializednão necessária
Exceção em erroUninitializedPropertyAccessExceptionerro no bloco init
Cachenão aplicávelcálculo único
Android BindingView Binding, Data Bindingnão usado
Frameworks DIDagger, Hilt, Koininjeção manual

Use lateinit quando uma propriedade precisar mudar após a inicialização ou sua criação for gerenciada por código externo. Um exemplo típico é o View Binding em Android Activity: o binding é criado no onCreate, mas permanece var porque o framework não suporta val para este cenário.

Use lazy quando uma propriedade for inicializada uma vez, seu cálculo for caro e o valor não mudar durante a vida do objeto. Um exemplo clássico é a criação preguiçosa de um cliente Retrofit ou banco de dados Room no primeiro acesso ao repositório.

Combinando lateinit e lazy

Ambos os mecanismos podem ser usados simultaneamente dentro de uma mesma classe. Por exemplo, lateinit para View Binding e lazy para um repositório. Esta é uma prática normal que reflete diferentes requisitos para diferentes propriedades. O principal é não confundir a semântica: não use lateinit onde val é necessário, e não use lazy para propriedades que precisam ser reatribuídas.

Erros comuns ao usar lateinit e lazy

O erro mais comum com lateinit é acessar a propriedade antes de ela ser inicializada. Isso leva ao UninitializedPropertyAccessException, que não é capturado em tempo de compilação porque Kotlin confia que o desenvolvedor garanta a ordem correta de inicialização. A solução é sempre verificar o estado via ::property.isInitialized antes do acesso em situações ambíguas.

O segundo problema comum é usar lateinit para propriedades que são semanticamente val. Se o valor é definido uma vez e nunca muda, lazy é a escolha mais correta. Torna a propriedade imutável, evita sobrescrita acidental e adiciona segurança de thread gratuitamente.

O terceiro erro é lazy com efeitos colaterais. O bloco de inicialização lazy não deve modificar o estado externo nem depender da ordem de inicialização de outras propriedades lazy, pois a sequência de cálculo depende do primeiro acesso e pode não ser óbvia. Se propriedades lazy referenciarem umas às outras, isso leva a dependência circular e StackOverflowError.

O quarto problema são vazamentos de memória via lazy no Android. Se um bloco lazy capturar uma referência a uma Activity ou Fragment, o delegado mantém o fechamento e o coletor de lixo não pode liberar o componente mesmo após sua destruição. A solução é usar lazy apenas com objetos de curta duração ou passar o contexto da Application em vez da Activity.

O quinto erro típico é tentar aplicar lateinit a tipos primitivos. O compilador Kotlin bloqueia isso no nível de sintaxe, mas os desenvolvedores tentam contornar a limitação através de wrappers anuláveis. Isso leva a verificações null desnecessárias e anula completamente os benefícios da inicialização diferida.

Perguntas frequentes

Qual é a diferença entre lateinit e lazy em Kotlin?

lateinit é um modificador para propriedades var, permitindo a inicialização após o construtor. lazy é um delegado para propriedades val, que calcula o valor no primeiro acesso e o armazena em cache. lateinit não suporta tipos primitivos e nullable, enquanto lazy é seguro para threads por padrão.

Posso verificar se uma propriedade lateinit foi inicializada?

Sim, através da referência embutida à propriedade: ::propertyName.isInitialized. O método retorna true se a propriedade já foi inicializada. Esta é a única maneira segura de evitar UninitializedPropertyAccessException ao trabalhar com campos lateinit.

Por que lateinit não pode ser usado com tipos primitivos?

Tipos primitivos — Int, Double, Boolean e outros — compilam para primitivos JVM (int, double, boolean), que não têm um estado “não inicializado”. lateinit usa null como um sinalizador, e primitivos não podem ser null, portanto o mecanismo é fisicamente impossível para esses tipos.

Qual é o modo de segurança de thread padrão do lazy?

O padrão é LazyThreadSafetyMode.SYNCHRONIZED — verificação dupla com bloqueio, garantindo inicialização única sob acesso concorrente de várias threads. Para cenários single-thread, use NONE; para alta contenção, use PUBLICATION.

Quando devo usar lateinit em vez de lazy no Android?

Quando a propriedade precisar mudar após a inicialização ou sua criação for gerenciada pelo framework. Um exemplo típico é o View Binding em Android Activity: o binding é criado no onCreate e deve ser var. Para dependências val inicializadas uma vez, use lazy.

Resumo

  • lateinit — um modificador para propriedades var do Kotlin, permitindo inicialização após o construtor sem nullable
  • lazy — um delegado de propriedade para val com cálculo único e armazenamento automático em cache do resultado
  • lateinit lança UninitializedPropertyAccessException ao acessar antes da inicialização
  • lazy é seguro para threads por padrão via LazyThreadSafetyMode.SYNCHRONIZED
  • lateinit é incompatível com tipos primitivos e propriedades anuláveis
  • lazy pode causar vazamentos de memória no Android ao capturar contexto em um fechamento
  • Escolha lateinit para propriedades mutáveis e lazy para dependências val inicializadas uma vez

Vamos desenvolver um aplicativo móvel chave na mão

A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.

Discutir o projeto

Leia também