Kotlin: Classes e Objetos do Kotlin Explicados

Última atualização: 2026-08-26

A filosofia de design de classes do Kotlin: construtor primário + propriedades em uma linha, blocos init substituindo o corpo do construtor Java — Charlie define a classe Order em 3 linhas que são mais poderosas que as 30 linhas equivalentes do Java.

1. O que Você Aprenderá


2. A História Real de um Arquiteto

(1) Problema: Inferno dos Construtores Java

Em Java, a classe Order do Charlie precisava de 3 construtores (sem argumentos, campos obrigatórios, todos os campos), além de getters/setters/lógica de validação — 80+ linhas para uma única classe de domínio.

(2) Solução: Construtor Primário do Kotlin

KOTLIN
// Java: 80+ linhas para uma classe de domínio
// Kotlin: 3 linhas com validação
class Order(val id: String, var status: String, val total: BigDecimal) {
    init { require(total >= BigDecimal.ZERO) { "Total deve ser não negativo" } }
}

Construtor primário + propriedades + bloco init combinados — 80 linhas de Java → 3 linhas de Kotlin.


3. Construtor Primário e Propriedades

(1) Construtor Primário em Uma Linha

KOTLIN
// Construtor primário com propriedades em uma linha
class Order(val id: String, var status: String, val total: Double)

// O equivalente Java teria 20+ linhas

(2) Parâmetros do Construtor vs Propriedades

KOTLIN
// val/var no construtor = propriedade
class Order(val id: String,    // propriedade somente leitura
            var status: String, // propriedade mutável
            total: Double)      // apenas parâmetro do construtor, não propriedade

// Uso
val order = Order("ORD-001", "PENDING", 299.99)
println(order.id)      // OK - propriedade val
order.status = "PAID"  // OK - propriedade var
// order.total         // ERRO - não é propriedade

(3) Construtor Primário vs Construtor Secundário

KOTLIN
class Order(val id: String, var status: String, val total: Double) {
    // Construtor secundário DEVE delegar ao primário
    constructor(id: String) : this(id, "PENDING", 0.0)

    // Outro construtor secundário
    constructor(id: String, total: Double) : this(id, "PENDING", total)
}

val order1 = Order("ORD-001")               // Secundário
val order2 = Order("ORD-002", 299.99)        // Secundário
val order3 = Order("ORD-003", "CONFIRMED", 1_500.00)  // Primário

(4) Comparação de Construtores

Dimensão Java Kotlin
Definir propriedades + construtor Separados Construtor primário em uma linha
Múltiplos construtores Definições independentes Secundário deve delegar ao primário
Parâmetro para propriedade Atribuição manual val/var automático
Valores padrão Sobrecarga de métodos Parâmetros padrão

4. Bloco init

O bloco init executa imediatamente após o construtor primário — usado para validação e lógica de inicialização.

(1) Uso Básico

KOTLIN
class Order(val id: String, var status: String, val total: Double) {
    init {
        require(total >= 0.0) { "Total deve ser não negativo, recebido $total" }
        require(id.startsWith("ORD-")) { "ID do pedido deve começar com ORD-" }
    }

    // Múltiplos blocos init executam em ordem
    init {
        println("Pedido $id criado com total \$$total USD")
    }
}

(2) Ordem de Execução do Bloco init

KOTLIN
class Example {
    val a = println("1: inicialização de propriedade")

    init {
        println("2: primeiro bloco init")
    }

    val b = println("3: inicialização de propriedade")

    init {
        println("4: segundo bloco init")
    }
}
// Saída: 1, 2, 3, 4 (ordem de declaração)

5. Modificadores de Visibilidade

(1) Quatro Níveis de Visibilidade

KOTLIN
class OrderProcessor {
    // public (padrão): visível em todo lugar
    fun process(order: Order) { ... }

    // private: visível apenas dentro desta classe
    private fun validate(order: Order) { ... }

    // protected: visível nesta classe e subclasses
    protected fun calculateTax(order: Order) { ... }

    // internal: visível dentro do mesmo módulo
    internal fun report() { ... }
}

(2) Tabela de Comparação de Visibilidade

Modificador Dentro da Classe Subclasse Mesmo Módulo Global
public
internal
protected
private

(3) Diferenças de Visibilidade: Java vs Kotlin

Dimensão Java Kotlin
Visibilidade padrão package-private public
Visibilidade de módulo Nenhuma internal
Visibilidade de pacote package-private Nenhuma (use internal em vez)
Declarações de nível superior apenas public public / internal / private

6. Inicialização Diferida

(1) lateinit var

KOTLIN
class OrderService {
    // lateinit: promessa de inicializar antes do uso
    lateinit var repository: OrderRepository

    fun init(repo: OrderRepository) {
        repository = repo
    }

    fun process(order: Order) {
        // Acesso antes da inicialização lança UninitializedPropertyAccessException
        repository.save(order)
    }
}

(2) by lazy

KOTLIN
class OrderProcessor {
    // lazy: seguro para threads, inicializado no primeiro acesso
    val cache: OrderCache by lazy {
        println("Inicializando cache...")
        OrderCache(maxSize = 10_000)
    }

    // lazy com modo de lock personalizado
    val heavyResource by lazy(LazyThreadSafetyMode.PUBLICATION) {
        loadHeavyResource()
    }
}

(3) Comparação lateinit vs by lazy

Dimensão lateinit by lazy
Tipo var (mutável) val (somente leitura)
Momento da inicialização Atribuição manual No primeiro acesso
Segurança de thread Não Sim (padrão)
Nulidade Declarado não nulo Declarado não nulo
Acesso antes de init Exceção em tempo de execução Nunca acontece
Caso de uso Injeção de framework DI Propriedades computadas custosas

7. Diagrama de Relacionamento de Classes

100%
classDiagram
    class Order {
        +val id: String
        +var status: String
        +val total: Double
        +val items: List~OrderItem~
        +fun addItem(item: OrderItem)
    }
    class OrderItem {
        +val sku: String
        +val quantity: Int
        +val unitPrice: Double
        +fun subtotal: Double
    }
    class Customer {
        +val id: String
        +val name: String
        +val email: String?
        +val address: Address?
    }
    class Address {
        +val street: String
        +val city: String
        +val country: String
    }
    Order --> OrderItem : contém
    Order --> Customer : pertence a
    Customer --> Address : tem

8. Exemplo Completo: Modelo de Domínio do OrderProcessor

KOTLIN
// ============================================
// OrderProcessor - Modelo de Domínio
// Funcionalidade: Order, OrderItem, Customer com validação init
// ============================================

import java.math.BigDecimal
import java.math.RoundingMode

class Address(val street: String, val city: String, val country: String) {
    override fun toString(): String = "$street, $city, $country"
}

class Customer(val id: String, val name: String, val email: String?) {
    var address: Address? = null

    fun getDisplayEmail(): String = email ?: "sem-email"

    override fun toString(): String = "Customer($id, $name, ${getDisplayEmail()})"
}

class OrderItem(val sku: String, val quantity: Int, val unitPrice: BigDecimal) {
    init {
        require(quantity > 0) { "Quantidade deve ser positiva, recebido $quantity" }
        require(unitPrice >= BigDecimal.ZERO) { "Preço deve ser não negativo" }
    }

    val subtotal: BigDecimal
        get() = unitPrice.multiply(BigDecimal(quantity)).setScale(2, RoundingMode.HALF_UP)
}

class Order(
    val id: String,
    var status: String,
    private val _items: MutableList<OrderItem> = mutableListOf()
) {
    init {
        require(id.startsWith("ORD-")) { "ID do pedido deve começar com ORD-" }
    }

    val items: List<OrderItem> get() = _items.toList()

    val total: BigDecimal
        get() = _items.fold(BigDecimal.ZERO) { acc, item -> acc.add(item.subtotal) }

    fun addItem(item: OrderItem) {
        _items.add(item)
    }

    val itemCount: Int get() = _items.size

    // Imposto computado de forma preguiçosa
    val tax by lazy {
        total.multiply(BigDecimal("0.08")).setScale(2, RoundingMode.HALF_UP)
    }

    override fun toString(): String = "Order($id, $status, ${itemCount} itens, \$$total USD)"
}

fun main() {
    val customer = Customer("CUST-001", "Alice", "alice@example.com").also {
        it.address = Address("123 Main St", "New York", "US")
    }
    println(customer)
    println("Endereço: ${customer.address}")

    val order = Order("ORD-001", "PENDING")
    order.addItem(OrderItem("SKU-WIDGET", 3, BigDecimal("9.99")))
    order.addItem(OrderItem("SKU-GADGET", 1, BigDecimal("149.99")))
    order.addItem(OrderItem("SKU-DOOHICKEY", 5, BigDecimal("4.50")))

    println(order)
    println("Subtotal: \$$${order.total} USD")
    println("Imposto: \$$${order.tax} USD")
    println("Total Geral: \$$${order.total.add(order.tax)} USD")

    order.status = "CONFIRMED"
    println("Status atualizado: ${order.status}")
}

Saída:

TEXT 📖 Somente leitura
Customer(CUST-001, Alice, alice@example.com)
Endereço: 123 Main St, New York, US
Order(ORD-001, PENDING, 3 itens, $199.42 USD)
Subtotal: $$199.42 USD
Imposto: $$15.95 USD
Total Geral: $$215.37 USD
Status atualizado: CONFIRMED

9. Exemplos práticos rápidos

▶ Exemplo: Classe básica com init

KOTLIN
class Order(
    val id: String,
    var status: String = "PENDING"
) {
    // Bloco init executado durante a construção
    val createdAt: Long = System.currentTimeMillis()

    init {
        println("Pedido $id criado (status: $status)")
    }

    // Bloco init secundário após validação
    init {
        require(id.startsWith("ORD-")) {
            "ID do pedido inválido: $id"
        }
    }

    fun confirm() {
        status = "CONFIRMED"
        println("$id confirmado")
    }
}

val order = Order("ORD-001")
order.confirm()
println("Criado em: ${order.createdAt}")

Saída:

TEXT 📖 Somente leitura
Pedido ORD-001 criado (status: PENDING)
ORD-001 confirmado
Criado em: 1700000000000

▶ Exemplo: Construtores primários e secundários

KOTLIN
// Construtor primário com propriedade em val/var
class Customer(val name: String, val email: String)

// Classe com construtor secundário
class VipCustomer(val name: String) {
    var tier: String = "GOLD"
    var points: Int = 0

    init {
        println("VIP $name criado")
    }

    // Construtor secundário deve chamar o primário
    constructor(name: String, tier: String, points: Int) : this(name) {
        this.tier = tier
        this.points = points
    }

    override fun toString() = "$name [$tier, $points pts]"
}

val c1 = Customer("Alice", "alice@example.com")
val c2 = VipCustomer("Bob")
val c3 = VipCustomer("Charlie", "PLATINUM", 5000)

println(c1)
println(c2)
println(c3)

Saída:

TEXT 📖 Somente leitura
VIP Bob criado
Customer(name=Alice, email=alice@example.com)
Bob [GOLD, 0 pts]
Charlie [PLATINUM, 5000 pts]

▶ Exemplo: Data classes

KOTLIN
// data class gera automaticamente equals/hashCode/toString/copy/componentN
data class Product(
    val sku: String,
    val name: String,
    var price: Double
)

val p1 = Product("SKU-001", "Caneta", 2.99)
val p2 = p1.copy(price = 3.49)  // cópia imutável com novos valores

println("p1: $p1")
println("p2: $p2")
println("Mesmo objeto? ${p1 == p2}")

// Destruturação
val (sku, name, price) = p2
println("sku=$sku nome=$name preço=$price")

// Cópia com mudança seletiva
val p3 = p1.copy(name = "Caneta Premium")
println("p3: $p3")

Saída:

TEXT 📖 Somente leitura
p1: Product(sku=SKU-001, name=Caneta, price=2.99)
p2: Product(sku=SKU-001, name=Caneta, price=3.49)
Mesmo objeto? false
sku=SKU-001 nome=Caneta preço=3.49
p3: Product(sku=SKU-001, name=Caneta Premium, price=2.99)

▶ Exemplo: Object expressions e companion objects

KOTLIN
// object expression: classe anônima de uso único
val logger = object {
    fun log(level: String, msg: String) {
        val ts = System.currentTimeMillis()
        println("[$ts][$level] $msg")
    }
}

logger.log("INFO", "Pedido recebido")
logger.log("ERROR", "Falha no pagamento")

// companion object: membro estático da classe
class ApiClient(val baseUrl: String) {
    companion object {
        private const val DEFAULT_TIMEOUT = 30

        fun createProduction(): ApiClient {
            return ApiClient("https://api.prod.com")
        }

        fun createDevelopment(): ApiClient {
            return ApiClient("http://localhost:8080")
        }
    }

    init {
        println("Cliente inicializado: $baseUrl, timeout=${DEFAULT_TIMEOUT}s")
    }
}

val prod = ApiClient.createProduction()
val dev = ApiClient.createDevelopment()

Saída:

TEXT 📖 Somente leitura
[1700000000000][INFO] Pedido recebido
[1700000000000][ERROR] Falha no pagamento
Cliente inicializado: https://api.prod.com, timeout=30s
Cliente inicializado: http://localhost:8080, timeout=30s

▶ Exemplo: Encapsulamento com visibilidade

KOTLIN
// private: apenas dentro da classe
// protected: classe e subclasses
// internal: dentro do módulo
// public (padrão): qualquer lugar

class BankAccount(
    val owner: String,
    initialBalance: Double
) {
    private var balance: Double = initialBalance
    private val transactions = mutableListOf<String>()

    init {
        require(initialBalance >= 0) { "Saldo inicial deve ser >= 0" }
        transactions.add("Depósito inicial: \$$initialBalance")
    }

    fun deposit(amount: Double) {
        require(amount > 0) { "Valor deve ser positivo" }
        balance += amount
        transactions.add("Depósito: \$$amount")
    }

    fun withdraw(amount: Double): Boolean {
        if (amount > balance) {
            transactions.add("Tentativa de saque falhou: \$$amount")
            return false
        }
        balance -= amount
        transactions.add("Saque: \$$amount")
        return true
    }

    fun getStatement(): String = buildString {
        append("Conta de $owner\n")
        append("Saldo: \$$balance USD\n")
        append("Transações:\n")
        transactions.forEach { append("  - $it\n") }
    }
}

val account = BankAccount("Alice", 1000.0)
account.deposit(500.0)
account.withdraw(200.0)
account.withdraw(2000.0)
account.deposit(100.0)
println(account.getStatement())

Saída:

TEXT 📖 Somente leitura
Conta de Alice
Saldo: $1400.0 USD
Transações:
  - Depósito inicial: $1000.0
  - Depósito: $500.0
  - Saque: $200.0
  - Tentativa de saque falhou: $2000.0
  - Depósito: $100.0

▶ Exemplo: Object declarations (singleton)

KOTLIN
// object declaration: singleton thread-safe automático
object DatabaseConnection {
    private val connectionPool = mutableMapOf<String, String>()

    init {
        println("Conexão com o banco de dados inicializada")
    }

    fun getConnection(key: String): String {
        return connectionPool.getOrPut(key) {
            "Connection[$key]@${System.identityHashCode(this)}"
        }
    }

    fun closeAll() {
        println("Fechando ${connectionPool.size} conexões")
        connectionPool.clear()
    }
}

val conn1 = DatabaseConnection.getConnection("order-db")
val conn2 = DatabaseConnection.getConnection("user-db")
val conn3 = DatabaseConnection.getConnection("order-db")  // mesmo objeto que conn1

println("conn1: $conn1")
println("conn2: $conn2")
println("conn3 == conn1: ${conn3 == conn1}")

DatabaseConnection.closeAll()

Saída:

TEXT 📖 Somente leitura
Conexão com o banco de dados inicializada
conn1: Connection[order-db]@12345678
conn2: Connection[user-db]@87654321
conn3 == conn1: true
Fechando 2 conexões

▶ Exemplo: Classes aninhadas e inner

KOTLIN
class OrderService(val name: String) {
    // Nested class - não guarda referência à classe externa
    class Config(val url: String, val timeout: Int)

    // Inner class - guarda referência à outer class
    inner class AuditLog {
        fun record(action: String) {
            println("[$name] $action")
        }
    }

    private val log = AuditLog()

    fun processOrder(id: String) {
        log.record("processando $id")
        log.record("validando $id")
        log.record("confirmando $id")
    }
}

val service = OrderService("OrderService-A")
service.processOrder("ORD-001")

// Nested class usada independentemente
val config = OrderService.Config("https://api.com", 30)
println("Config: $config")

Saída:

TEXT 📖 Somente leitura
[OrderService-A] processando ORD-001
[OrderService-A] validando ORD-001
[OrderService-A] confirmando ORD-001
Config: Config(url=https://api.com, timeout=30)

❓ Perguntas Frequentes

P: Construtores primário e secundário podem coexistir? R: Sim, mas construtores secundários devem delegar ao primário usando this(...). Parâmetros padrão são preferidos em vez de construtores secundários.

P: Posso ter múltiplos blocos init? R: Sim, múltiplos blocos init executam na ordem de declaração. É recomendado mesclá-los em um para evitar confusão.

P: lateinit pode ser usado para tipos primitivos? R: Não. lateinit só funciona com tipos não primitivos (tipos objeto), porque primitivos têm valores padrão. Use by lazy ou tipos anuláveis + padrões para Int/Double etc.

P: A inicialização by lazy é segura para threads? R: Sim por padrão (modo SYNCHRONIZED). Se você tem certeza de acesso single-thread, use LazyThreadSafetyMode.NONE para melhor desempenho.

P: Como a visibilidade internal funciona em projetos multi-módulo Maven/Gradle? R: internal restringe a visibilidade dentro do mesmo módulo Gradle ou Maven. Diferentes módulos — mesmo dentro do mesmo projeto — não podem acessar membros internal.

P: Por que o Kotlin não tem package-private do Java? R: O Kotlin usa internal (visibilidade em nível de módulo) em vez de package-private. Visibilidade em nível de pacote é frequentemente mal utilizada em Java; nível de módulo se adapta melhor às estruturas de projeto modernas.


📖 Resumo


📝 Exercícios

  1. Iniciante (⭐): Defina uma classe Product (id: String, name: String, price: Double) com um bloco init validando price >= 0. Dica: require(price >= 0)
  2. Intermediário (⭐⭐): Projete uma classe Service usando lateinit para injetar Repository e by lazy para inicializar um cache. Dica: lateinit + by lazy
  3. Desafio (⭐⭐⭐): Implemente um modelo de domínio Order completo com Order/OrderItem/Customer, toda validação em blocos init e cópia defensiva para itens. Dica: consulte o exemplo completo da Seção 8

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