Introdução: o dilema eterno do desenvolvedor Kotlin
Se você já escreveu em Kotlin e precisou adiar a inicialização de uma propriedade, certamente já se perguntou: "O que escolher — by lazy ou lateinit?" Esta pergunta é um verdadeiro sucesso no Stack Overflow (milhares de visualizações, dezenas de discussões). E não é à toa: da escolha correta dependem não apenas a legibilidade do código, mas também o desempenho da aplicação.
Neste artigo, não vamos apenas recontar a documentação. Vamos conduzir uma verdadeira investigação: analisar o funcionamento interno de ambos os mecanismos, comparar sua velocidade, consumo de memória e dar recomendações claras. E no final — resumiremos com ações concretas. Vamos nessa!
O que são lateinit e by lazy? Breve introdução
Para começar, vamos refrescar os conceitos básicos. Ambas as abordagens resolvem o mesmo problema: adiar a inicialização de uma propriedade até o momento do seu primeiro uso. Mas fazem isso de maneiras diferentes.
lateinit var — uma promessa ao compilador
A palavra-chave lateinit diz ao compilador: "Eu prometo que inicializarei esta propriedade antes do primeiro acesso, por enquanto apenas reserve espaço para ela". É usada apenas com var e apenas para propriedades mutáveis. Ideal para injeção de dependência (Dagger, Hilt, Koin) ou para propriedades que são inicializadas no onCreate de uma Activity/Fragment.
class MyActivity : AppCompatActivity() { lateinit var repository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) repository = UserRepository() // inicialização }
fun loadUser() { // Se repository não for inicializado — ocorrerá UninitializedPropertyAccessException repository.getUser() }}by lazy — delegado preguiçoso
O delegado lazy é um mecanismo mais flexível. Ele cria um objeto wrapper que, no primeiro acesso, executa uma lambda e armazena o resultado. Todos os acessos subsequentes apenas retornam o valor armazenado. Por padrão, é thread-safe (sincronizado), mas é possível configurar o modo.
class MyViewModel : ViewModel() { val heavyData: ExpensiveData by lazy { // Este bloco será executado apenas uma vez, no primeiro acesso ExpensiveData() }}Ambas as abordagens resolvem o problema da "inicialização preguiçosa", mas fazem isso com diferentes compromissos. Vamos analisar esses compromissos em detalhes.
Desempenho: qual é mais rápido?
Aqui chegamos ao mais interessante — aos números. Desempenho não é apenas a velocidade de acesso à propriedade, mas também a sobrecarga de inicialização, memória e segurança de thread.
Velocidade do primeiro acesso
lateinit — o recordista absoluto. No primeiro acesso, o compilador apenas verifica se o campo não é nulo (se for nullable — lança uma exceção). Sem lambdas, sem sincronizações. É praticamente gratuito.
by lazy — mais lento. No primeiro acesso, um bloco sincronizado é chamado (se o modo padrão for LazyThreadSafetyMode.SYNCHRONIZED), a lambda é executada e o resultado é armazenado em cache. Isso leva microssegundos, mas em loops de alta carga ou em código crítico pode ser perceptível.
Velocidade de acessos repetidos
Após a inicialização, ambas as abordagens funcionam igualmente rápido. lateinit apenas lê o campo (como um var comum). by lazy após a primeira inicialização também lê o valor em cache. A diferença de velocidade nesta etapa é praticamente zero.
Consumo de memória
Aqui, lateinit vence de lavada. Ele não c