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á


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

KOTLIN
// 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 class gera 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

KOTLIN
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

KOTLIN
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())
⚠️ Nota: 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

KOTLIN
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

KOTLIN
// 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

KOTLIN
// 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

KOTLIN
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

100%
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

KOTLIN
// 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

KOTLIN
// ============================================
// 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:

TEXT 📖 Somente leitura
=== 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

KOTLIN
// 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:

TEXT 📖 Somente leitura
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

KOTLIN
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:

TEXT 📖 Somente leitura
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

KOTLIN
// 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:

TEXT 📖 Somente leitura
Criado com 3 itens
Pago $299.99
Enviado: TRK-123
Cancelado: Solicitação do cliente

▶ Exemplo: Sealed interface

KOTLIN
// 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:

TEXT 📖 Somente leitura
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

KOTLIN
// 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:

TEXT 📖 Somente leitura
[1] Aguardando pagamento
[2] Confirmado
[3] Enviado: TRK-...

▶ Exemplo: Data classes em coleções

KOTLIN
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:

TEXT 📖 Somente leitura
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

KOTLIN
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:

TEXT 📖 Somente leitura
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


📝 Exercícios

  1. Iniciante (⭐): Use data class para definir Product e teste copy(), equals() e declarações de desestruturação. Dica: data class Product(val name: String, val price: Double)
  2. Intermediário (⭐⭐): Use sealed class para modelar Result como Success/Failure, e escreva handleResult para tratar ambos os casos. Dica: sealed class Result { data class Success(...) ... }
  3. 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 do when + require()

← Anterior | Próximo →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%