Kotlin: Data Classes e Sealed Classes do Kotlin Explicadas
Última atualização: 2026-08-26
data class é o recurso matador do Kotlin para eliminar boilerplate — 1 linha gera automaticamente equals/hashCode/toString/copy/componentN. sealed class constrói hierarquias de tipos finitas que permitem ao compilador garantir a exaustividade — Charlie a usa para modelar a máquina de estados de pedidos sem casos perdidos.
1. O que Você Aprenderá
- 6 métodos gerados automaticamente de
data class - Declarações de desestruturação:
val (id, total) = order sealed classcom ramificaçõeswhenexaustivas- Modelando máquinas de estados com sealed classes
- Charlie em ação:
data class+sealed class
2. A História Real de um Desenvolvedor
(1) Ponto de Dor: equals Escrito à Mão Causando Bugs
Bob escreveu manualmente um método equals para a classe Order em Java, mas esqueceu de comparar o campo status. Como resultado, a deduplicação do Set tratou pedidos com status diferentes como o mesmo pedido, causando 200 operações de processamento duplicadas.
(2) Solução com data class Gerada Automaticamente
// Java: 30 linhas de equals/hashCode propensas a erros
// Kotlin: 1 linha, gerada pelo compilador e correta
data class Order(val id: String, val total: Double, val status: String)
// equals compara TODAS as propriedades do construtor - impossível esquecer um campo
data classgera automaticamente equals/hashCode com base em todas as propriedades do construtor — impossível esquecer um campo.
3. Geração Automática do data class
(1) Uso Básico
data class Order(val id: String, val total: Double, val status: String)
val o1 = Order("ORD-001", 299.99, "CONFIRMED")
val o2 = Order("ORD-001", 299.99, "CONFIRMED")
// equals gerado automaticamente: compara todas as propriedades do construtor
println(o1 == o2) // true
// toString gerado automaticamente
println(o1) // Order(id=ORD-001, total=299.99, status=CONFIRMED)
// copy gerado automaticamente: criar instância modificada
val updated = o1.copy(status = "SHIPPED")
println(updated) // Order(id=ORD-001, total=299.99, status=SHIPPED)
// hashCode gerado automaticamente
println(o1.hashCode() == o2.hashCode()) // true
(2) 6 Métodos Gerados Automaticamente
| Método | Finalidade | Baseado Em |
|---|---|---|
equals() |
Verificação de igualdade de valor | Todas as propriedades do construtor |
hashCode() |
Cálculo de hash | Todas as propriedades do construtor |
toString() |
Representação em string | Order(id=..., total=..., status=...) |
copy() |
Criar cópia modificada | Novos valores de propriedades especificadas |
component1() |
Declaração de desestruturação | Por ordem de declaração da propriedade |
componentN() |
Declaração de desestruturação | Propriedade N → componentN |
(3) Aviso sobre Cópia Profunda do copy
data class Order(val id: String, val items: MutableList<String>)
val o1 = Order("ORD-001", mutableListOf("Widget", "Gadget"))
val o2 = o1.copy() // Cópia superficial!
o2.items.add("Doohickey")
println(o1.items) // [Widget, Gadget, Doohickey] - referência compartilhada!
// Correção: copiar explicitamente a lista
val o3 = o1.copy(items = o1.items.toMutableList())
copy() é uma cópia superficial — propriedades de tipo de referência ainda compartilham o mesmo objeto. Você deve tratar a cópia profunda manualmente quando necessário.
4. Declarações de Desestruturação
(1) Desestruturação Básica
data class Order(val id: String, val total: Double, val status: String)
val order = Order("ORD-001", 299.99, "CONFIRMED")
// Declaração de desestruturação
val (id, total, status) = order
println("ID: $id, Total: \$$total USD, Status: $status")
// Desestruturação parcial com _
val (orderId, _, orderStatus) = order // Pular total
// Em loop for
val orders = listOf(order, Order("ORD-002", 1_500.00, "PENDING"))
for ((oid, ototal, ostatus) in orders) {
println("$oid: \$$ototal USD ($ostatus)")
}
(2) Casos de Uso de Desestruturação
// Entradas de Map
val statusMap = mapOf("ORD-001" to "SHIPPED", "ORD-002" to "PENDING")
for ((id, status) in statusMap) { ... }
// Valores de retorno de função
data class Result(val success: Boolean, val message: String)
val (ok, msg) = processOrder()
// Parâmetros de lambda
orders.map { (id, total) -> "Order $id: \$$total USD" }
5. sealed class
(1) Sealed Class Constrói Hierarquias de Tipos Finitas
// Sealed class: hierarquia de classes restrita
// Todas as subclasses DEVEM estar no mesmo arquivo (Kotlin 1.5: mesmo pacote)
sealed class OrderStatus {
object Pending : OrderStatus()
data class Processing(val step: Int, val totalSteps: Int) : OrderStatus()
object Shipped : OrderStatus()
data class Delivered(val deliveredAt: String) : OrderStatus()
data class Cancelled(val reason: String) : OrderStatus()
}
(2) Ramificações when Exaustivas
fun handleStatus(status: OrderStatus): String = when (status) {
is OrderStatus.Pending -> "Aguardando pagamento"
is OrderStatus.Processing -> "Etapa ${status.step}/${status.totalSteps}"
is OrderStatus.Shipped -> "Em trânsito"
is OrderStatus.Delivered -> "Entregue em ${status.deliveredAt}"
is OrderStatus.Cancelled -> "Cancelado: ${status.reason}"
// Nenhum else necessário - o compilador verifica se todos os casos foram cobertos
}
(3) Máquina de Estados com Sealed Class
stateDiagram-v2
[*] --> Pending
Pending --> Processing : Pay
Pending --> Cancelled : Cancel
Processing --> Shipped : Ship
Processing --> Cancelled : Fail
Shipped --> Delivered : Deliver
Delivered --> [*]
Cancelled --> [*]
(4) sealed class vs enum class vs open class
| Dimensão | enum class |
sealed class |
open class |
|---|---|---|---|
| Quantidade de subclasses | Valores fixos do enum | Finita (conhecida em tempo de compilação) | Infinita (extensível em tempo de execução) |
| Estado da subclasse | Singleton | Estado independente cada | Estado independente cada |
| Exaustividade do when | ✅ | ✅ | ❌ (requer else) |
| Granularidade da subclasse | Mesma classe | Classes diferentes | Classes diferentes |
| Extensibilidade | Não extensível | Extensão limitada | Totalmente aberta |
| Caso de uso | Enums simples | Tipos finitos + estado | Herança aberta |
6. Charlie em Ação: Event Sourcing de Pedidos
// Eventos de domínio para event sourcing
sealed class OrderEvent {
data class Created(val orderId: String, val total: Double, val customer: String) : OrderEvent()
data class PaymentReceived(val orderId: String, val amount: Double) : OrderEvent()
data class ItemAdded(val orderId: String, val sku: String, val qty: Int) : OrderEvent()
data class Shipped(val orderId: String, val trackingCode: String) : OrderEvent()
data class Cancelled(val orderId: String, val reason: String) : OrderEvent()
}
data class OrderState(val id: String, val total: Double, val status: String, val items: List<String>)
fun applyEvent(state: OrderState?, event: OrderEvent): OrderState = when (event) {
is OrderEvent.Created -> OrderState(event.orderId, event.total, "PENDING", emptyList())
is OrderEvent.PaymentReceived -> state!!.copy(status = "PAID")
is OrderEvent.ItemAdded -> state!!.copy(items = state.items + "${event.sku}x${event.qty}")
is OrderEvent.Shipped -> state!!.copy(status = "SHIPPED")
is OrderEvent.Cancelled -> state!!.copy(status = "CANCELLED")
}
7. Exemplo Completo: OrderProcessor Máquina de Estados
// ============================================
// OrderProcessor - Máquina de Estados com Sealed Classes
// Recurso: Ciclo de vida de pedidos com when exaustivo
// ============================================
sealed class OrderStatus {
object Pending : OrderStatus()
data class Processing(val step: Int, val totalSteps: Int) : OrderStatus()
object Shipped : OrderStatus()
data class Delivered(val date: String) : OrderStatus()
data class Cancelled(val reason: String, val refundAmount: Double) : OrderStatus()
}
data class Order(val id: String, val total: Double, var status: OrderStatus = OrderStatus.Pending)
fun describeStatus(status: OrderStatus): String = when (status) {
is OrderStatus.Pending -> "Aguardando pagamento"
is OrderStatus.Processing -> "Processando etapa ${status.step}/${status.totalSteps}"
is OrderStatus.Shipped -> "Em trânsito"
is OrderStatus.Delivered -> "Entregue em ${status.date}"
is OrderStatus.Cancelled -> "Cancelado: ${status.reason} (reembolso: \$${status.refundAmount} USD)"
}
fun canTransitionTo(current: OrderStatus, next: OrderStatus): Boolean = when {
current is OrderStatus.Pending && next is OrderStatus.Processing -> true
current is OrderStatus.Processing && next is OrderStatus.Shipped -> true
current is OrderStatus.Shipped && next is OrderStatus.Delivered -> true
current is OrderStatus.Pending && next is OrderStatus.Cancelled -> true
current is OrderStatus.Processing && next is OrderStatus.Cancelled -> true
else -> false
}
fun main() {
val orders = listOf(
Order("ORD-001", 299.99, OrderStatus.Pending),
Order("ORD-002", 1_500.00, OrderStatus.Processing(2, 5)),
Order("ORD-003", 8_900.00, OrderStatus.Shipped),
Order("ORD-004", 45.50, OrderStatus.Delivered("2026-01-15")),
Order("ORD-005", 2_500.00, OrderStatus.Cancelled("Customer request", 2_500.00))
)
println("=== Relatório de Status dos Pedidos ===")
orders.forEach { order ->
println("${order.id}: ${describeStatus(order.status)} (Total: \$${order.total} USD)")
}
// Testar transições
val testOrder = Order("ORD-099", 100.00)
println("\n=== Testes de Transição ===")
println("Pending -> Processing(1,3): ${canTransitionTo(testOrder.status, OrderStatus.Processing(1, 3))}")
println("Pending -> Shipped: ${canTransitionTo(testOrder.status, OrderStatus.Shipped)}")
println("Pending -> Cancelled: ${canTransitionTo(testOrder.status, OrderStatus.Cancelled("Error", 100.0))}")
// Agregar por tipo de status
val statusCounts = orders.groupBy { it.status::class.simpleName }
println("\n=== Distribuição de Status ===")
statusCounts.forEach { (status, list) -> println(" $status: ${list.size} pedidos") }
}
Saída:
=== Relatório de Status dos Pedidos ===
ORD-001: Aguardando pagamento (Total: $299.99 USD)
ORD-002: Processando etapa 2/5 (Total: $1500.0 USD)
ORD-003: Em trânsito (Total: $8900.0 USD)
ORD-004: Entregue em 2026-01-15 (Total: $45.5 USD)
ORD-005: Cancelado: Customer request (reembolso: $2500.0 USD) (Total: $2500.0 USD)
=== Testes de Transição ===
Pending -> Processing(1,3): true
Pending -> Shipped: false
Pending -> Cancelled: true
=== Distribuição de Status ===
Pending: 1 pedidos
Processing: 1 pedidos
Shipped: 1 pedidos
Delivered: 1 pedidos
Cancelled: 1 pedidos
9. Exemplos práticos rápidos
▶ Exemplo: Data class básica
// data class gera equals/hashCode/toString/copy/componentN
data class Product(
val sku: String,
val name: String,
val price: Double
)
val p1 = Product("SKU-001", "Caneta", 2.99)
val p2 = Product("SKU-001", "Caneta", 2.99)
println("p1: $p1")
println("p1 == p2: ${p1 == p2}") // true (compara valores)
println("p1 === p2: ${p1 === p2}") // false (compara referências)
val p3 = p1.copy(price = 3.99) // cópia imutável
println("p3: $p3")
Saída:
p1: Product(sku=SKU-001, name=Caneta, price=2.99)
p1 == p2: true
p1 === p2: false
p3: Product(sku=SKU-001, name=Caneta, price=3.99)
▶ Exemplo: Destructuring declarations
data class Customer(val name: String, val email: String, val tier: String)
val customer = Customer("Alice", "alice@example.com", "GOLD")
// Destruturação por posição
val (name, email, tier) = customer
println("Nome: $name | Email: $email | Tier: $tier")
// Em lambdas: par de valores
val map = mapOf(
"ORD-001" to 299.99,
"ORD-002" to 1500.00,
"ORD-003" to 50.00
)
for ((orderId, price) in map) {
println("$orderId: \$$price USD")
}
// Ignorando elementos
val (_, _, t) = customer
println("Tier ignorado nome/email: $t")
Saída:
Nome: Alice | Email: alice@example.com | Tier: GOLD
ORD-001: $299.99 USD
ORD-002: $1500.0 USD
ORD-003: $50.0 USD
Tier ignorado nome/email: GOLD
▶ Exemplo: Sealed classes - hierarquia fechada
// Sealed: subclasses devem estar no mesmo arquivo (antes de Kotlin 1.5)
// Agora podem estar no mesmo pacote ou módulo
sealed class OrderEvent {
abstract val orderId: String
data class Created(override val orderId: String, val items: Int) : OrderEvent()
data class Paid(override val orderId: String, val amount: Double) : OrderEvent()
data class Shipped(override val orderId: String, val tracking: String) : OrderEvent()
data class Cancelled(override val orderId: String, val reason: String) : OrderEvent()
}
// Quando exaustivo (sem else) - verificador de compilador
fun describe(event: OrderEvent) = when (event) {
is OrderEvent.Created -> "Criado com ${event.items} itens"
is OrderEvent.Paid -> "Pago \$${event.amount}"
is OrderEvent.Shipped -> "Enviado: ${event.tracking}"
is OrderEvent.Cancelled -> "Cancelado: ${event.reason}"
// Sem else - compilador confirma que todos os casos estão cobertos
}
val events = listOf(
OrderEvent.Created("ORD-001", 3),
OrderEvent.Paid("ORD-001", 299.99),
OrderEvent.Shipped("ORD-001", "TRK-123"),
OrderEvent.Cancelled("ORD-002", "Solicitação do cliente")
)
events.forEach { println("${describe(it)}") }
Saída:
Criado com 3 itens
Pago $299.99
Enviado: TRK-123
Cancelado: Solicitação do cliente
▶ Exemplo: Sealed interface
// sealed interface: alternativa moderna a sealed class
// Permite múltiplas implementações em arquivos diferentes dentro do mesmo módulo
sealed interface PaymentMethod {
fun charge(amount: Double): String
}
data class CreditCard(val last4: String) : PaymentMethod {
override fun charge(amount: Double) = "Cobrado \$$amount do cartão ****$last4"
}
data class PayPal(val email: String) : PaymentMethod {
override fun charge(amount: Double) = "Cobrado \$$amount via PayPal ($email)"
}
data class Crypto(val currency: String) : PaymentMethod {
override fun charge(amount: Double) = "Enviado \$$amount em $currency"
}
val methods: List<PaymentMethod> = listOf(
CreditCard("1234"),
PayPal("user@example.com"),
Crypto("BTC")
)
methods.forEach { println(it.charge(99.99)) }
Saída:
Cobrado $99.99 do cartão ****1234
Cobrado $99.99 via PayPal (user@example.com)
Enviado $99.99 em BTC
▶ Exemplo: State pattern com sealed
// Sealed class ideal para máquinas de estado
sealed class OrderState {
abstract fun next(): OrderState
abstract fun statusText(): String
}
object Pending : OrderState() {
override fun next() = Confirmed
override fun statusText() = "Aguardando pagamento"
}
data class Confirmed(val paidAt: Long = System.currentTimeMillis()) : OrderState() {
override fun next() = Shipped
override fun statusText() = "Confirmado"
}
data class Shipped(val tracking: String) : OrderState() {
override fun next() = Delivered
override fun statusText() = "Enviado: $tracking"
}
object Delivered : OrderState() {
override fun next(): OrderState = this // estado final
override fun statusText() = "Entregue"
}
var state: OrderState = Pending
println("[1] ${state.statusText()}")
state = state.next()
println("[2] ${state.statusText()}")
state = state.next()
println("[3] ${state.statusText()}")
Saída:
[1] Aguardando pagamento
[2] Confirmado
[3] Enviado: TRK-...
▶ Exemplo: Data classes em coleções
data class Employee(val name: String, val department: String, val salary: Double)
val employees = listOf(
Employee("Alice", "Engenharia", 120_000.0),
Employee("Bob", "Engenharia", 110_000.0),
Employee("Charlie", "Vendas", 95_000.0),
Employee("Diana", "Vendas", 105_000.0),
Employee("Eve", "Engenharia", 130_000.0)
)
// Agrupar por departamento
val byDept = employees.groupBy { it.department }
byDept.forEach { (dept, list) ->
val avg = list.map { it.salary }.average()
println("$dept: ${list.size} pessoas, salário médio: \$$avg")
}
// Encontrar pessoas com salário mais alto por departamento
val topPerDept = byDept.mapValues { (_, list) -> list.maxByOrNull { it.salary }!! }
println("\nTop por departamento:")
topPerDept.forEach { (dept, emp) -> println("$dept: ${emp.name} (\$${emp.salary})") }
Saída:
Engenharia: 3 pessoas, salário médio: $120000.0
Vendas: 2 pessoas, salário médio: $100000.0
Top por departamento:
Engenharia: Eve ($130000.0)
Vendas: Diana ($105000.0)
▶ Exemplo: copy() seletivo de data class
data class CartItem(val sku: String, val quantity: Int, val unitPrice: Double)
val original = CartItem("SKU-001", 2, 29.99)
// copy() preserva campos não especificados
val updated1 = original.copy(quantity = 5) // duplica a quantidade
val updated2 = original.copy(quantity = 1, unitPrice = 24.99) // promo
println("Original: $original")
println("Qtd 5: $updated1 -> \$${updated1.quantity * updated1.unitPrice}")
println("Promo: $updated2 -> \$${updated2.quantity * updated2.unitPrice}")
// copy() é fundamental para programação funcional/imutável
val withTax = original.copy(unitPrice = original.unitPrice * 1.08)
println("Com imposto: $withTax -> \$${withTax.quantity * withTax.unitPrice}")
Saída:
Original: CartItem(sku=SKU-001, quantity=2, unitPrice=29.99)
Qtd 5: CartItem(sku=SKU-001, quantity=5, unitPrice=29.99) -> $149.95
Promo: CartItem(sku=SKU-001, quantity=1, unitPrice=24.99) -> $24.99
Com imposto: CartItem(sku=SKU-001, quantity=2, unitPrice=32.3892) -> $64.7784
❓ Perguntas Frequentes
P: Uma data class pode herdar de outras classes? R: Pode herdar de classes não-data, mas não pode herdar de outra data class. Data classes podem implementar interfaces.
P: O copy do data class é uma cópia profunda? R: Não.
copy()é uma cópia superficial — propriedades de tipo de referência ainda compartilham o mesmo objeto. A cópia profunda deve ser tratada manualmente.
P: As subclasses de sealed class devem estar no mesmo arquivo? R: Antes do Kotlin 1.5, precisavam estar no mesmo arquivo. A partir do 1.5+, só precisam estar no mesmo pacote. Isso permite maior flexibilidade organizacional.
P: Qual a diferença entre sealed interface e sealed class? R: sealed interface (Kotlin 1.5+) permite múltiplas implementações, enquanto sealed class suporta apenas herança simples. sealed interface é mais flexível.
P: Por que sealed classes podem omitir else no when? R: O compilador conhece todas as subclasses de uma sealed class, então pode verificar se todos os casos foram cobertos no
when. Adicionar uma nova subclasse causará um erro de compilação, lembrando você de adicionar a ramificação.
P: Todos os parâmetros do construtor primário em uma data class devem ser val/var? R: Não, mas apenas propriedades val/var participam de equals/hashCode/copy/toString. Parâmetros comuns não participam.
📖 Resumo
data classgera automaticamente equals/hashCode/toString/copy/componentN — 1 linha substitui 60+ linhascopy()é uma cópia superficial — propriedades de tipo de referência ainda compartilham objetos- Declarações de desestruturação
val (a, b) = objsão baseadas em componentN gerado automaticamente sealed classconstrói hierarquias de tipos finitas — o compilador garante a exaustividade dowhen- Sealed classes são ideais para modelar máquinas de estados, tipos de eventos e tipos de resultado
sealed class>enum: subclasses podem cada uma armazenar dados de estado diferentes
📝 Exercícios
- Iniciante (⭐): Use
data classpara definirProducte testecopy(),equals()e declarações de desestruturação. Dica:data class Product(val name: String, val price: Double) - Intermediário (⭐⭐): Use
sealed classpara modelarResultcomoSuccess/Failure, e escrevahandleResultpara tratar ambos os casos. Dica:sealed class Result { data class Success(...) ... } - Desafio (⭐⭐⭐): Modele uma máquina de estados completa de pedidos usando sealed classes (Pending→Processing→Shipped→Delivered / Cancelled), implemente
transition()que lança exceção em transições de estado ilegais. Dica: verificação exaustiva dowhen+require()