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á
- Construtor primário + propriedades em uma linha
- Blocos
init: lógica de validação - Visibilidade:
public/internal/protected/private - Inicialização diferida:
lateiniteby lazy - Charlie em ação: projetando classes de domínio Order / OrderItem / Customer
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
// 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
// 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
// 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
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
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
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
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
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
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
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
// ============================================
// 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:
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
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:
Pedido ORD-001 criado (status: PENDING)
ORD-001 confirmado
Criado em: 1700000000000
▶ Exemplo: Construtores primários e secundários
// 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:
VIP Bob criado
Customer(name=Alice, email=alice@example.com)
Bob [GOLD, 0 pts]
Charlie [PLATINUM, 5000 pts]
▶ Exemplo: Data classes
// 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:
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
// 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:
[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
// 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:
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)
// 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:
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
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:
[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 lazyou 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.NONEpara 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
- Construtor primário +
val/vardefine propriedades em uma linha, eliminando o boilerplate de getter/setter do Java - Blocos
initsubstituem o corpo do construtor Java para validação e inicialização - Quatro níveis de visibilidade:
public(padrão) /internal/protected/private lateinitserve para injeção de framework DI;by lazyserve para computação diferida- Campos de suporte de propriedade usam o padrão
private var+ visualização pública somente leitura para cópia defensiva - Parâmetros padrão são preferidos em vez de construtores secundários para manter construtores limpos
📝 Exercícios
- Iniciante (⭐): Defina uma classe
Product(id: String, name: String, price: Double) com um bloco init validandoprice >= 0. Dica:require(price >= 0) - Intermediário (⭐⭐): Projete uma classe
Serviceusandolateinitpara injetarRepositoryeby lazypara inicializar um cache. Dica:lateinit+by lazy - 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