Data Class e Copy em Kotlin
1. Visão geral
data class é um dos recursos que fazem Kotlin escrever código muito mais conciso que Java quando você trabalha com classes que apenas contêm dados (DTO, model, value object). Este artigo ajuda você a entender o que Kotlin gera automaticamente para uma data class, como usar copy() para criar cópias com alguns campos modificados, e as armadilhas comuns como comparações incorretas, usar var em data class, ou herança de data class.
2. O Que É Uma Data Class?
Quando você declara uma classe com a palavra-chave data, o compilador Kotlin gera automaticamente alguns métodos com base nas propriedades declaradas no construtor primário.
data class Customer(
val id: Long,
val name: String,
val email: String
)
Com apenas estas 4 linhas, Kotlin gera automaticamente:
equals()/hashCode()baseado emid,name,email.toString()no formatoCustomer(id=1, name=..., email=...).copy()para criar cópias com alguns campos alterados.componentN()para suportar declaração destructuring.
Comparado com Java, você teria que escrever uma classe com construtor, getters, equals, hashCode, toString — cerca de 40-50 linhas para a mesma funcionalidade.
2.1. toString() Padrão
fun main() {
val customer = Customer(1, "An Nguyen", "an@nonladev.com")
println(customer)
}
Customer(id=1, name=An Nguyen, email=an@nonladev.com)
O toString() gerado automaticamente é muito conveniente ao logar ou debugar, você não precisa escrever um.
3. equals() e hashCode() Por Valor
A maior diferença entre data class e uma classe comum é que equals() compara por valor (igualdade estrutural) em vez de por referência.
fun main() {
val c1 = Customer(1, "An Nguyen", "an@nonladev.com")
val c2 = Customer(1, "An Nguyen", "an@nonladev.com")
println(c1 == c2) // compara por valor
println(c1 === c2) // compara por referência
}
true
false
==chamaequals(), dois objetos com os mesmos campos são iguais, mesmo que sejam referências diferentes.===verifica se as duas variáveis apontam para o mesmo objeto na memória.
Nota: Com uma classe comum (não uma
data class),==por padrão compara por referência como===, a menos que você sobrescreva manualmenteequals().
Como hashCode() também é gerado com base nas propriedades, você pode usar data class como chave em HashMap ou elemento em HashSet de forma segura.
fun main() {
val set = hashSetOf(
Customer(1, "An Nguyen", "an@nonladev.com"),
Customer(1, "An Nguyen", "an@nonladev.com")
)
println(set.size)
}
1
Dois objetos com os mesmos valores são considerados iguais, o HashSet mantém apenas 1 elemento.
4. Função copy() — Criando Cópias Imutáveis
Se você usar val para as propriedades, Customer é imutável — você não pode modificar os campos diretamente após criação. Para “alterar” alguns valores, Kotlin gera a função copy() que permite criar um novo objeto, mantendo inalterados os campos que não foram passados.
fun main() {
val original = Customer(1, "An Nguyen", "an@nonladev.com")
val updated = original.copy(email = "an.new@nonladev.com")
println(original)
println(updated)
}
Customer(id=1, name=An Nguyen, email=an@nonladev.com)
Customer(id=1, name=An Nguyen, email=an.new@nonladev.com)
originalnão é alterado em absoluto — esta é a essência da imutabilidade.updatedé um novo objeto, que difere apenas ememail, os outros campos são mantidos conforme estavam.
Este é um padrão muito comum ao trabalhar com estado em UI (Compose, MVI) ou ao atualizar uma entidade em lógica de negócio sem querer alterar o objeto diretamente.
4.1. copy() Com Argumentos Nomeados
Você pode passar múltiplos parâmetros de uma vez, pelo nome (named arguments) para evitar confusão de ordem:
val promoted = original.copy(name = "An Nguyen (VIP)", email = "vip@nonladev.com")
Se não usar argumentos nomeados mas seguir a ordem de posição, o código é fácil de confundir quando a classe tem muitos campos do mesmo tipo String.
5. Shallow Copy — Armadilha Com Objetos Mutáveis Dentro
copy() apenas cria uma shallow copy: copia referências dos campos, não copia profundamente objetos complexos dentro.
data class Order(
val id: Long,
val items: MutableList<String>
)
fun main() {
val order1 = Order(1, mutableListOf("Book"))
val order2 = order1.copy()
order2.items.add("Pen")
println(order1.items) // é afetada!
println(order2.items)
}
[Book, Pen]
[Book, Pen]
order2 = order1.copy()apenas cria um novo objetoOrder, mas o campoitemsainda aponta para a mesmaMutableList.- Modificar
order2.itemsfazorder1.itemsmudar também — este é um erro muito comum ao usar coleções mutáveis em data class.
Como corrigir isto: use List imutável (toList()) ou copie manualmente a coleção no construtor.
data class Order(
val id: Long,
val items: List<String>
)
fun main() {
val order1 = Order(1, listOf("Book"))
val order2 = order1.copy(items = order1.items + "Pen")
println(order1.items)
println(order2.items)
}
[Book]
[Book, Pen]
Use List<String> imutável e o operador + para criar uma nova lista em vez de alterar, mantendo o espírito da imutabilidade de data class.
Nota: Se você precisar de um deep copy real (nested data class contendo outra data class), você deve escrever manualmente a lógica recursiva de cópia, Kotlin não gera deep copy automaticamente.
6. Declaração Destructuring
Graças ao componentN() gerado automaticamente, você pode “destructurar” uma data class em múltiplas variáveis separadas:
fun main() {
val customer = Customer(1, "An Nguyen", "an@nonladev.com")
val (id, name, email) = customer
println("$id - $name - $email")
}
1 - An Nguyen - an@nonladev.com
A ordem das variáveis corresponde à ordem das propriedades no construtor primário, não em nome. Alterar a ordem das propriedades declaradas tornará destructuring incorreto — você precisa tomar cuidado ao refatorar.
Nota: Você deve fazer destructuring apenas quando a data class tem poucos campos (2-3). Com classes com muitos campos, use nomes de propriedade diretamente para tornar o código mais fácil de ler.
7. Limitações da Data Class
7.1. Não Pode Ser Herdada
data class não pode herdar de outra data class (não pode ser open junto com data da forma que herda campos automaticamente):
// Erro de compilação se tentar isto:
// data class VipCustomer(val tier: String) : Customer(...)
Kotlin apenas permite que data class implemente interface, ou herde de uma classe comum (non-data) que não tenha propriedades no construtor primário. Razão: equals()/copy() gerados automaticamente serão muito difíceis de definir corretamente quando há herança multi-nível de campos.
Solução comum: composição em vez de herança.
data class VipCustomer(
val customer: Customer,
val tier: String
)
7.2. Construtor Primário Deve Ter Pelo Menos 1 Propriedade
// Erro: data class deve ter pelo menos 1 parâmetro no construtor primário
// data class Empty
7.3. Apenas Propriedades no Construtor Primário São Consideradas
data class Product(val name: String) {
var discount: Int = 0
}
fun main() {
val p1 = Product("Laptop").apply { discount = 10 }
val p2 = Product("Laptop").apply { discount = 20 }
println(p1 == p2) // true, discount não participa de equals()
}
true
discount é declarado no corpo, não está no construtor primário, então não é considerado em equals(), hashCode(), toString(), copy(). Este é um erro fácil de se confundir — se um campo é importante para comparação, deve ser colocado no construtor primário.
8. Testando Com JUnit 5 e AssertJ
Dependências necessárias (Gradle Kotlin DSL):
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
testImplementation("org.assertj:assertj-core:3.26.0")
Escreva testes para verificar o comportamento de copy() e equals():
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
class CustomerTest {
@Test
fun `copy should keep unchanged fields and update specified ones`() {
val original = Customer(1, "An Nguyen", "an@nonladev.com")
val updated = original.copy(email = "an.new@nonladev.com")
assertThat(updated.id).isEqualTo(original.id)
assertThat(updated.name).isEqualTo(original.name)
assertThat(updated.email).isEqualTo("an.new@nonladev.com")
assertThat(original.email).isEqualTo("an@nonladev.com")
}
@Test
fun `two customers with same fields should be equal`() {
val c1 = Customer(1, "An Nguyen", "an@nonladev.com")
val c2 = Customer(1, "An Nguyen", "an@nonladev.com")
assertThat(c1).isEqualTo(c2)
assertThat(c1).isNotSameAs(c2)
}
}
isEqualToverifica a igualdade por valor, correto com a essência deequals()dedata class.isNotSameAsconfirma quec1ec2são dois objetos diferentes na memória, embora sejam iguais em valor.- O primeiro teste confirma que
copy()funciona corretamente: campos não especificados são mantidos, campos especificados são atualizados, o objeto original não é modificado.
9. Quando Não Usar Data Class
- Entity JPA/Hibernate:
equals()/hashCode()em todos os campos (incluindo campos gerados comoidantes de persistir) é fácil causar erros ao usar emHashSetou comparar entidades já salvas/não salvas no BD. Deve-se escreverequals()/hashCode()manualmente baseado emidpara entidades. - Classe com lógica de negócio complexa:
data classé destinada para data holder, não é apropriado quando a classe precisa de muitos comportamentos e herança em múltiplos níveis. - Classe que precisa otimizar memória com muitos campos mutáveis aninhados: considere usar builder pattern ou uma classe comum se shallow copy causa riscos difíceis de controlar.
10. Conclusão
data class ajuda você a eliminar boilerplate code ao trabalhar com DTO/model em Kotlin: gera automaticamente equals(), hashCode(), toString(), copy() e suporta destructuring. A função copy() é uma ferramenta poderosa para manter imutabilidade, mas você precisa lembrar que ela apenas cria shallow copy — tenha cuidado com campos contendo coleções ou objetos mutáveis dentro. Evite usar data class para entidades JPA ou classes com lógica de negócio complexa, e sempre coloque campos importantes no construtor primário para que equals()/copy() funcionem corretamente como esperado.