Neste tutorial, você vai aprender tudo sobre Data Classes em Kotlin — um recurso que elimina toneladas de código boilerplate que você precisaria escrever em Java. Data classes geram automaticamente métodos como equals(), hashCode(), toString(), copy() e funções componentN() para destructuring. Ao final deste guia, você saberá quando e como usá-las de forma eficiente no seu código.
O Problema que Data Classes Resolvem
Em Java, para representar uma simples classe de dados como um Usuario, você precisaria escrever manualmente: construtor, getters, setters, equals(), hashCode() e toString(). São facilmente mais de 50 linhas de código para algo conceitualmente simples.
Em Kotlin, uma data class resolve tudo isso em uma única linha:
data class Usuario(val nome: String, val email: String, val idade: Int)
Essa única linha gera automaticamente todos os métodos mencionados acima. Vamos explorar cada um deles em detalhes.
Sintaxe e Regras Básicas
Para declarar uma data class, basta usar a palavra-chave data antes de class. Existem algumas regras que o compilador exige:
// Data class válida
data class Produto(val nome: String, val preco: Double, val categoria: String)
// O construtor primário PRECISA ter pelo menos um parâmetro
// Todos os parâmetros do construtor primário devem ser val ou var
Regras obrigatórias para data classes:
- O construtor primário precisa ter pelo menos um parâmetro
- Cada parâmetro do construtor primário deve ser marcado como
valouvar - Data classes não podem ser
abstract,open,sealedouinner
Métodos Gerados Automaticamente
toString()
O toString() gerado é extremamente útil para debugging e logging:
data class Livro(val titulo: String, val autor: String, val paginas: Int)
val livro = Livro("O Senhor dos Anéis", "Tolkien", 1200)
println(livro)
// Livro(titulo=O Senhor dos Anéis, autor=Tolkien, paginas=1200)
Compare com uma classe regular, que imprimiria algo como Livro@1a2b3c4d. A data class mostra todos os valores de forma legível.
equals() e hashCode()
Data classes comparam por valor, não por referência. Dois objetos com os mesmos dados são considerados iguais:
data class Ponto(val x: Int, val y: Int)
val p1 = Ponto(3, 7)
val p2 = Ponto(3, 7)
val p3 = Ponto(1, 5)
println(p1 == p2) // true — mesmos valores
println(p1 == p3) // false — valores diferentes
println(p1 === p2) // false — referências diferentes (são objetos distintos)
// hashCode também é consistente
val conjunto = hashSetOf(p1, p2, p3)
println(conjunto.size) // 2 — p1 e p2 são "iguais"
Isso é fundamental para usar data classes como chaves em Map, em Set, ou em qualquer situação que dependa de igualdade estrutural.
copy()
O método copy() cria uma cópia do objeto, permitindo alterar apenas algumas propriedades. Isso é essencial para trabalhar com objetos imutáveis (declarados com val):
data class Configuracao(
val tema: String,
val idioma: String,
val notificacoes: Boolean,
val fontSize: Int
)
val configOriginal = Configuracao("escuro", "pt-BR", true, 16)
// Alterar apenas o tema e o tamanho da fonte
val configNova = configOriginal.copy(tema = "claro", fontSize = 18)
println(configOriginal)
// Configuracao(tema=escuro, idioma=pt-BR, notificacoes=true, fontSize=16)
println(configNova)
// Configuracao(tema=claro, idioma=pt-BR, notificacoes=true, fontSize=18)
O copy() é a base para o padrão de imutabilidade em Kotlin. Em vez de modificar um objeto existente, você cria uma nova versão com as alterações desejadas.
componentN() e Destructuring
Data classes geram funções component1(), component2(), etc., correspondendo à ordem dos parâmetros no construtor. Isso habilita o destructuring:
data class Endereco(val rua: String, val numero: Int, val cidade: String)
val endereco = Endereco("Av. Paulista", 1000, "São Paulo")
// Destructuring declaration
val (rua, numero, cidade) = endereco
println("$rua, $numero — $cidade")
// Av. Paulista, 1000 — São Paulo
// Funciona em loops também
val enderecos = listOf(
Endereco("Rua A", 10, "SP"),
Endereco("Rua B", 20, "RJ"),
Endereco("Rua C", 30, "BH")
)
for ((r, n, c) in enderecos) {
println("$r, $n — $c")
}
// E com lambdas!
enderecos.forEach { (r, _, c) -> // _ ignora o número
println("$r em $c")
}
Propriedades Fora do Construtor Primário
Propriedades declaradas dentro do corpo da data class não participam dos métodos gerados:
data class Aluno(val nome: String, val matricula: Int) {
var notaFinal: Double = 0.0 // NÃO participa de equals, hashCode, toString, copy
}
val a1 = Aluno("Maria", 12345)
a1.notaFinal = 9.5
val a2 = Aluno("Maria", 12345)
a2.notaFinal = 7.0
println(a1 == a2) // true! — notaFinal NÃO é considerada
println(a1) // Aluno(nome=Maria, matricula=12345) — sem notaFinal
Isso é intencional: apenas as propriedades do construtor primário definem a “identidade” do objeto. Use essa separação estrategicamente.
Data Class vs Classe Regular
Quando usar data class e quando usar uma classe regular? Aqui está um comparativo claro:
// USE Data Class quando:
// - O objetivo principal é armazenar dados
// - Você precisa de equals/hashCode baseado em valores
// - Quer destructuring ou copy()
data class PedidoItem(val produtoId: Long, val quantidade: Int, val precoUnitario: Double)
// USE Classe Regular quando:
// - A classe tem comportamento complexo
// - Identidade é baseada em referência, não em valores
// - Precisa de herança (data classes não podem ser open)
class CarrinhoDeCompras {
private val itens = mutableListOf<PedidoItem>()
fun adicionar(item: PedidoItem) { itens.add(item) }
fun total(): Double = itens.sumOf { it.precoUnitario * it.quantidade }
fun limpar() { itens.clear() }
}
Limitações de Data Classes
Existem limitações importantes que você deve conhecer:
// 1. Data classes NÃO podem ser open (não permitem herança direta)
// data class Base(val x: Int) — não pode ser herdada
// class Filha(x: Int, val y: Int) : Base(x) — ERRO!
// 2. Solução: use interfaces ou sealed classes
interface Forma {
fun area(): Double
}
data class Circulo(val raio: Double) : Forma {
override fun area() = Math.PI * raio * raio
}
data class Retangulo(val largura: Double, val altura: Double) : Forma {
override fun area() = largura * altura
}
// 3. Data classes podem implementar interfaces normalmente
// 4. Data classes PODEM ser combinadas com sealed classes
Exemplo Prático Completo
Vamos criar um sistema de gerenciamento de tarefas usando data classes:
enum class Prioridade { BAIXA, MEDIA, ALTA, URGENTE }
enum class Status { PENDENTE, EM_ANDAMENTO, CONCLUIDA, CANCELADA }
data class Tarefa(
val id: Long,
val titulo: String,
val descricao: String,
val prioridade: Prioridade,
val status: Status = Status.PENDENTE,
val tags: List<String> = emptyList()
)
fun main() {
val tarefas = listOf(
Tarefa(1, "Configurar CI/CD", "Pipeline do GitHub Actions", Prioridade.ALTA, tags = listOf("devops")),
Tarefa(2, "Corrigir bug login", "Erro 401 no OAuth", Prioridade.URGENTE, Status.EM_ANDAMENTO, listOf("bug", "auth")),
Tarefa(3, "Atualizar README", "Documentar nova API", Prioridade.BAIXA, tags = listOf("docs")),
Tarefa(4, "Refatorar módulo X", "Aplicar SOLID", Prioridade.MEDIA, tags = listOf("refactor"))
)
// Filtrar e transformar usando destructuring
val urgentes = tarefas
.filter { it.prioridade == Prioridade.URGENTE || it.prioridade == Prioridade.ALTA }
.sortedBy { it.prioridade }
println("=== Tarefas Prioritárias ===")
urgentes.forEach { (id, titulo, _, prioridade, status) ->
println("#$id — $titulo [$prioridade] ($status)")
}
// Usar copy para atualizar status
val tarefaAtualizada = tarefas[0].copy(status = Status.CONCLUIDA)
println("\nAtualizada: $tarefaAtualizada")
// Agrupar por status
val porStatus = tarefas.groupBy { it.status }
porStatus.forEach { (status, lista) ->
println("\n$status: ${lista.map { it.titulo }}")
}
}
Erros Comuns
Colocar propriedades mutáveis no construtor primário: usar
varno construtor de data classes pode gerar bugs, poisequals()ehashCode()dependem dos valores. Se o valor mudar após inserir o objeto em umSetou como chave de umMap, você pode perder o acesso a ele. Prefiraval.Esperar que propriedades do corpo participem de equals(): como vimos, propriedades declaradas dentro do corpo da classe são ignoradas pelos métodos gerados. Isso é confuso para iniciantes e pode gerar comparações inesperadas.
Tentar herdar de uma data class: data classes não podem ser
open. Se você precisa de hierarquias, use sealed classes combinadas com data classes ou use interfaces.Abusar de data classes para tudo: nem toda classe deve ser uma data class. Classes com comportamento complexo, efeitos colaterais no construtor ou identidade baseada em referência devem ser classes regulares.
Esquecer que copy() é shallow: o
copy()cria uma cópia rasa. Se a data class contém listas ou outros objetos mutáveis, a cópia compartilhará as mesmas referências internas.
Conclusão e Próximos Passos
Data classes são um dos recursos mais práticos de Kotlin, eliminando centenas de linhas de boilerplate e tornando o código mais seguro e expressivo. Você aprendeu a sintaxe, os métodos gerados automaticamente, destructuring, o método copy(), as diferenças entre data classes e classes regulares, e as limitações que precisa conhecer.
Para continuar aprendendo, explore estes tópicos relacionados:
- Sealed Classes para criar hierarquias de tipos com data classes
- Extension Functions para adicionar funcionalidades às suas data classes
- Lambdas para trabalhar com coleções de data classes de forma funcional
- Generics para criar data classes parametrizadas
Comece a usar data classes sempre que precisar representar dados. Elas são a ferramenta certa para DTOs, modelos de domínio, respostas de API e qualquer estrutura cujo propósito principal seja carregar informação.