O que é Immutable em Kotlin?

Imutabilidade (immutability) é o princípio de que um valor, uma vez criado, não pode ser alterado. Em Kotlin, a imutabilidade e fortemente encorajada e suportada pela linguagem em vários niveis: variaveis com val, coleções somente leitura, data classes com propriedades val e value classes.

Código imutavel é mais fácil de entender, testar e paralelizar. Quando um objeto não muda, você não precisa se preocupar com quem mais pode estar modificando ele ao mesmo tempo.

val vs var: o primeiro nível

A forma mais básica de imutabilidade em Kotlin e usar val em vez de var:

val nome = "Kotlin"    // Imutavel: nao pode ser reatribuido
var idade = 10         // Mutavel: pode ser reatribuido

// nome = "Java"       // Erro de compilacao!
idade = 11             // OK

Porem, val garante apenas que a referência não muda. O objeto apontado ainda pode ser mutavel:

val lista = mutableListOf(1, 2, 3)
lista.add(4)  // OK! A referência nao mudou, mas o conteudo sim
// lista = mutableListOf(5) // Erro! Nao pode reatribuir val

Coleções somente leitura

Kotlin distingue entre coleções somente leitura e mutaveis:

// Somente leitura: nao expoe metodos de mutacao
val nomes: List<String> = listOf("Ana", "Bruno", "Carlos")
// nomes.add("Diana")  // Erro de compilacao!

// Mutavel: permite adicionar, remover, alterar
val nomesMutaveis: MutableList<String> = mutableListOf("Ana", "Bruno")
nomesMutaveis.add("Carlos")  // OK

A mesma distincao existe para Set/MutableSet e Map/MutableMap:

val mapaLeitura: Map<String, Int> = mapOf("a" to 1, "b" to 2)
val mapaMutavel: MutableMap<String, Int> = mutableMapOf("a" to 1)
mapaMutavel["c"] = 3  // OK

val conjuntoLeitura: Set<Int> = setOf(1, 2, 3)
val conjuntoMutavel: MutableSet<Int> = mutableSetOf(1, 2)
conjuntoMutavel.add(3)  // OK

Importante: as coleções “somente leitura” do Kotlin não são verdadeiramente imutáveis na JVM. Elas podem ser convertidas com cast para a versão mutavel. Para imutabilidade real, considere bibliotecas como kotlinx.collections.immutable.

Data classes imutáveis

O padrão recomendado e criar data classes com todas as propriedades val:

data class Usuario(
    val id: Long,
    val nome: String,
    val email: String
)

val usuario = Usuario(1, "Ana", "ana@email.com")
// usuario.nome = "Maria"  // Erro! Propriedade val

// Para "modificar", crie uma copia com copy()
val usuarioAtualizado = usuario.copy(nome = "Maria")
println(usuario)            // Usuario(id=1, nome=Ana, email=ana@email.com)
println(usuarioAtualizado)  // Usuario(id=1, nome=Maria, email=maria@email.com)

O método copy() cria uma nova instancia com os campos alterados, sem modificar o original. Isso e a essencia da programação com dados imutáveis.

Imutabilidade profunda vs rasa

Imutabilidade rasa (shallow) significa que a referência não muda, mas objetos internos podem:

data class Departamento(val nome: String, val membros: MutableList<String>)

val dept = Departamento("TI", mutableListOf("Ana", "Bruno"))
dept.membros.add("Carlos")  // Compila! A lista interna e mutavel

Para imutabilidade profunda (deep), todos os campos internos também devem ser imutáveis:

data class DepartamentoImutavel(val nome: String, val membros: List<String>)

val dept = DepartamentoImutavel("TI", listOf("Ana", "Bruno"))
// dept.membros.add("Carlos")  // Erro! List nao tem add
val deptAtualizado = dept.copy(membros = dept.membros + "Carlos")

Coleções persistentes com kotlinx.collections.immutable

Para imutabilidade garantida e eficiente, use a biblioteca oficial:

import kotlinx.collections.immutable.*

val lista = persistentListOf(1, 2, 3)
val novaLista = lista.add(4)  // Retorna nova lista, original inalterada

println(lista)      // [1, 2, 3]
println(novaLista)  // [1, 2, 3, 4]

val mapa = persistentMapOf("a" to 1, "b" to 2)
val novoMapa = mapa.put("c", 3)

Coleções persistentes usam estruturas de dados eficientes (como tries) que compartilham memória entre versões, evitando copiar tudo a cada modificacao.

Imutabilidade em concorrência

A principal vantagem prática da imutabilidade e em código concorrente:

// Seguro para acesso concorrente: dados nunca mudam
data class Configuração(
    val maxConexoes: Int,
    val timeout: Long,
    val habilitarCache: Boolean
)

val config = Configuração(10, 5000L, true)

// Pode ser lido por qualquer thread sem sincronizacao
launch(Dispatchers.Default) { usarConfig(config) }
launch(Dispatchers.IO) { usarConfig(config) }

Objetos imutáveis não precisam de locks, mutex ou qualquer mecanismo de sincronizacao. Eles são inerentemente thread-safe.

Sealed classes e imutabilidade

Sealed classes combinam bem com imutabilidade para modelar estados:

sealed class EstadoDaTela {
    object Carregando : EstadoDaTela()
    data class Sucesso(val dados: List<String>) : EstadoDaTela()
    data class Erro(val mensagem: String) : EstadoDaTela()
}

// Cada estado e imutavel; transicoes criam novos objetos
var estado: EstadoDaTela = EstadoDaTela.Carregando
estado = EstadoDaTela.Sucesso(listOf("item1", "item2"))

Quando usar imutabilidade

  • Sempre que possível: a regra geral em Kotlin e preferir val sobre var e List sobre MutableList.
  • Modelos de dados: data classes com propriedades val são o padrão ouro para representar dados.
  • Estado de UI: em Compose e arquiteturas MVI/MVVM, o estado e imutavel e atualizado por substituicao.
  • Concorrência: sempre que dados são compartilhados entre threads ou coroutines.
  • APIs publicas: expor coleções somente leitura impede que consumidores modifiquem dados internos.

Casos de Uso no Mundo Real

  1. Gerenciamento de estado em aplicações Compose/MVI: Em arquiteturas reativas como MVI, o estado da tela e representado por data classes imutaveis. Cada interação do usuário gera um novo estado via copy(), garantindo que a UI sempre reflita um snapshot consistente e que o histórico de estados possa ser rastreado para debugging.

  2. Cache distribuido e serialização: Objetos imutaveis sao ideais para caching porque sua identidade nunca muda apos a criação. Em sistemas distribuidos com Redis ou Memcached, dados imutaveis podem ser serializados e compartilhados entre instancias sem risco de inconsistencia.

  3. Event sourcing e auditoria: Sistemas financeiros e de compliance registram eventos como objetos imutaveis. Cada transacao bancaria, por exemplo, e representada como uma data class imutavel que nunca e alterada, criando um log de auditoria confiavel.

  4. Configuracoes de aplicação: Objetos de configuração carregados no startup da aplicação sao naturalmente imutaveis. Uma vez lidos do arquivo ou variavel de ambiente, eles sao compartilhados entre todas as threads sem necessidade de sincronizacao.

Boas Praticas

  • Declare propriedades como val por padrão e só mude para var quando houver uma necessidade concreta de mutabilidade.
  • Retorne List em vez de MutableList em APIs publicas. Use .toList() para criar uma copia somente leitura quando necessário.
  • Em data classes, utilize apenas propriedades val e tipos imutaveis nos campos para garantir imutabilidade profunda.
  • Considere a biblioteca kotlinx.collections.immutable quando precisar de garantias reais de imutabilidade e performance com colecoes persistentes.
  • Use copy() para criar versões modificadas de objetos imutaveis em vez de tornar propriedades mutaveis.

Perguntas Frequentes

P: Qual a diferenca entre uma colecao somente leitura e uma colecao verdadeiramente imutavel? R: Uma colecao somente leitura (List, Map, Set) apenas esconde os métodos de mutação na interface, mas a implementação subjacente pode ser mutavel e acessivel via cast. Uma colecao verdadeiramente imutavel, como as da biblioteca kotlinx.collections.immutable, garante que o conteudo nunca pode ser alterado, mesmo com casts ou reflexao.

P: Usar imutabilidade não causa problemas de performance por criar muitas copias? R: Na maioria dos casos, o impacto e insignificante. O garbage collector da JVM e otimizado para objetos de vida curta. Para colecoes grandes, estruturas persistentes (como as de kotlinx.collections.immutable) compartilham dados entre versões, evitando copias completas.

P: Quando e aceitavel usar dados mutaveis em Kotlin? R: Mutabilidade e aceitavel em escopos locais e controlados, como variaveis dentro de uma função, builders durante a construcao de objetos, e em cenários de performance crítica onde a criação de copias seria um gargalo mensuravel.

P: Como garantir imutabilidade profunda em uma data class com listas? R: Declare os campos de colecao com tipos somente leitura (List, Set, Map) e nunca passe instancias de MutableList diretamente. Use .toList() no construtor ou considere usar persistentListOf() da biblioteca de colecoes imutaveis.

Erros comuns

  1. Assumir que val significa imutavel: val impede reatribuicao da referência, mas o objeto pode ser mutavel internamente. Uma val lista: MutableList ainda permite modificacoes.

  2. Expor MutableList como retorno: retornar uma MutableList de uma função permite que o chamador modifique a colecao interna. Retorne List ou use .toList().

  3. Usar data class com propriedades var: isso quebra o contrato de imutabilidade e causa bugs sutis com hashCode/equals.

  4. Copiar tudo desnecessariamente: em coleções grandes, criar copias completas a cada modificacao e ineficiente. Use coleções persistentes.

  5. Esquecer imutabilidade profunda: ter um data class com val mas com campos que são MutableList internamente.

Termos relacionados

  • val: palavra-chave que declara uma referência somente leitura (não reatribuivel).
  • var: palavra-chave que declara uma referência mutavel.
  • Data Class: classe que gera automaticamente copy(), facilitando a criação de versões modificadas de objetos imutáveis.
  • Value Class: tipo inline que encapsula um valor de forma imutavel sem overhead.
  • Sealed Class: hierarquia fechada que modela estados imutáveis de forma segura.
  • Collections: a biblioteca padrão distingue entre coleções somente leitura e mutaveis.

Imutabilidade e um dos pilares do Kotlin idiomatico. A linguagem facilita a escrita de código imutavel em todos os niveis, desde variaveis simples até estruturas de dados complexas. Adotar imutabilidade como padrão torna o código mais previsivel, testavel e seguro em ambientes concorrentes.