Kotlin: Funções de Extensão do Kotlin Explicadas
Última atualização: 2026-08-26
Funções de extensão permitem que Charlie adicione isHighValue() a Order sem modificar seu código-fonte — não é mágica, apenas açúcar sintático de despacho estático em tempo de compilação, mas seu valor prático é inegável.
1. O que Você Aprenderá
- Funções de extensão:
fun ReceiverType.extensionName() - Propriedades de extensão: propriedades computadas sem campos de apoio
- Despacho estático: extensões não quebram encapsulamento, não participam de polimorfismo
- Controle de escopo: extensões de nível superior vs membros de classe
- Charlie em ação:
Order.isHighValue()/List<Order>.totalRevenue()
2. A História Real de um Arquiteto
(1) Ponto de Dor: Proliferação de Classes Util
O projeto Java de Charlie tem 15 classes Util: OrderUtil, StringUtil, DateUtil... Cada uma é um monte de métodos estáticos, tornando chamadas como OrderUtil.isHighValue(order) verbosas e pouco intuitivas.
(2) A Solução com Funções de Extensão
// Em vez de OrderUtil.isHighValue(order)
fun Order.isHighValue() = total > 10_000
// Agora chame como um método membro!
if (order.isHighValue()) {
routeToVipPipeline()
}
Funções de extensão transformam chamadas de API de
Util.method(obj)paraobj.method(), um salto quântico na legibilidade do código.
3. Fundamentos de Funções de Extensão
(1) Sintaxe Básica
// Estender String com formatação de ID de pedido
fun String.toOrderId() = "ORD-$this"
// Estender Double com formatação USD
fun Double.toUSD() = "\$$this USD"
// Estender List com lógica de negócio
fun List<Order>.totalRevenue() = this.sumOf { it.total }
// Uso
println("001".toOrderId()) // ORD-001
println(299.99.toUSD()) // $299.99 USD
println(orders.totalRevenue()) // 12345.67
(2) Funções de Extensão com Generics
// Extensão genérica
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
// Extensão com restrição de tipo
fun <T : Comparable<T>> List<T>.secondLargest(): T? {
return this.sortedDescending().secondOrNull()
}
(3) Extensões com Receptor Nullable
// Estender tipo nullable - tratar null graciosamente
fun String?.orDefault(default: String = "N/A"): String = this ?: default
val name: String? = null
println(name.orDefault("Unknown")) // Unknown
println("Alice".orDefault()) // Alice
4. Propriedades de Extensão
// Propriedade de extensão somente leitura
val BigDecimal.inMillions: Double
get() = this.toDouble() / 1_000_000
val String.isOrderId: Boolean
get() = startsWith("ORD-")
// Uso
val revenue = BigDecimal("2_500_000")
println("${revenue.inMillions}M") // 2.5M
println("ORD-001".isOrderId) // true
println("ABC-001".isOrderId) // false
// Nota: propriedades de extensão NÃO PODEM ter campos de apoio
// propriedades de extensão var são possíveis, mas requerem setter explícito
(1) Propriedades de Extensão vs Propriedades Membro
| Dimensão | Propriedade Membro | Propriedade de Extensão |
|---|---|---|
| Local de definição | Dentro da classe | Fora da classe |
| Campo de apoio | Possui um | Nenhum |
| Armazenamento de estado | Armazenado no objeto | Não pode armazenar |
| Mutabilidade | Pode ser var | var computada apenas |
| Nível de acesso | Sujeito à visibilidade | Acessa apenas API pública |
5. Despacho Estático — O Núcleo das Extensões
Funções de extensão são despachadas estaticamente: qual implementação é chamada é determinada pelo tipo declarado, não pelo tipo em tempo de execução.
(1) Exemplo de Despacho Estático
open class Order(val id: String, val total: Double)
class BulkOrder(id: String, total: Double, val minQty: Int) : Order(id, total)
// Extensão em Order
fun Order.summary() = "Order $id: \$$total USD"
// Extensão em BulkOrder
fun BulkOrder.summary() = "Bulk $id: \$$total USD (min: $minQty)"
fun printSummary(order: Order) {
println(order.summary()) // Sempre chama Order.summary()!
}
val bulk = BulkOrder("ORD-001", 5_000.0, 100)
printSummary(bulk) // "Order ORD-001: $5000.0 USD" - NÃO é a versão BulkOrder!
bulk.summary() // "Bulk ORD-001: $5000.0 USD (min: 100)" - chamada direta OK
(2) Diagrama de Sequência do Despacho Estático
sequenceDiagram
participant Caller
participant Order
participant BulkOrder
Caller->>Order: order.summary() (tipo declarado: Order)
Note over Order: Resolvido em TEMPO DE COMPILAÇÃO
Order-->>Caller: Resultado de Order.summary()
Caller->>BulkOrder: bulk.summary() (tipo declarado: BulkOrder)
Note over BulkOrder: Resolvido em TEMPO DE COMPILAÇÃO
BulkOrder-->>Caller: Resultado de BulkOrder.summary()
(3) Despacho Estático vs Despacho de Método Virtual
| Dimensão | Método Membro (Despacho Virtual) | Função de Extensão (Despacho Estático) |
|---|---|---|
| Momento da resolução | Tempo de execução | Tempo de compilação |
| Base | Tipo real | Tipo declarado |
| Polimorfismo | Suportado | Não suportado |
| Sobrescrita | Subclasse pode sobrescrever | Não pode sobrescrever |
| Vantagem | Flexibilidade dinâmica | Seguro e previsível |
6. Controle de Escopo
(1) Extensões de Nível Superior
// Arquivo: OrderExtensions.kt
package com.order.extensions
fun Order.isHighValue() = total > 10_000
fun List<Order>.totalRevenue() = sumOf { it.total }
(2) Extensões Membro de Classe
class OrderService {
// Extensão definida dentro de uma classe - visível apenas dentro desta classe
fun Order.needsReview(): Boolean = total > 5_000 && status == "PENDING"
fun process(order: Order) {
if (order.needsReview()) { // Acessível aqui
routeToReview(order)
}
}
}
// order.needsReview() // ERRO: não acessível fora de OrderService
(3) Comparação de Escopo de Extensão
| Local de Definição | Visibilidade | Caso de Uso |
|---|---|---|
| Nível superior (nível de arquivo) | Todo o projeto (após import) | Extensões utilitárias gerais |
| Dentro de membro de classe | Apenas dentro da classe | Extensões ligadas ao estado da classe |
| Mesmo arquivo | Apenas mesmo arquivo | Extensões auxiliares |
7. Exemplo Completo: Kit de Extensões do OrderProcessor
// ============================================
// OrderProcessor - Kit de Extensões
// Recurso: Lógica de negócio como funções de extensão
// ============================================
import java.math.BigDecimal
import java.math.RoundingMode
data class Order(val id: String, val total: Double, val status: String, val customer: String)
// Extensões de String
fun String.toOrderId() = if (startsWith("ORD-")) this else "ORD-$this"
fun String.isOrderId() = matches(Regex("ORD-\\d{3,}"))
// Extensões de Double
fun Double.toUSD(): String = "\$${"%.2f".format(this)} USD"
fun Double.inMillions(): Double = this / 1_000_000
// Extensões de BigDecimal
fun BigDecimal.toUSD(): String = "\$${setScale(2, RoundingMode.HALF_UP)} USD"
// Extensões de Order
fun Order.isHighValue() = total > 10_000
fun Order.isPending() = status == "PENDING"
fun Order.summary() = "$id | ${total.toUSD()} | $status | $customer"
// Extensões de List<Order>
fun List<Order>.totalRevenue() = sumOf { it.total }
fun List<Order>.highValueOrders() = filter { it.isHighValue() }
fun List<Order>.byCustomer() = groupBy { it.customer }
fun List<Order>.revenueByCustomer() = byCustomer().mapValues { (_, orders) -> orders.totalRevenue() }
// Extensão nullable
fun String?.orDefault(default: String = "UNKNOWN") = this ?: default
fun main() {
val orders = listOf(
Order("ORD-001", 299.99, "CONFIRMED", "Alice"),
Order("ORD-002", 15_000.00, "PENDING", "Bob"),
Order("ORD-003", 2_500.00, "SHIPPED", "Charlie"),
Order("ORD-004", 8_900.00, "CONFIRMED", "Bob"),
Order("ORD-005", 45.50, "CANCELLED", "Alice")
)
// Extensão de String
println("001".toOrderId()) // ORD-001
println("ORD-001".toOrderId()) // ORD-001
println("ORD-001".isOrderId()) // true
// Extensões de Order
println("\n=== Resumos dos Pedidos ===")
orders.forEach { println(it.summary()) }
println("\n=== Pedidos de Alto Valor ===")
orders.highValueOrders().forEach { println(it.summary()) }
// Extensões de List
println("\n=== Receita ===")
println("Total: ${orders.totalRevenue().toUSD()}")
println("Em milhões: ${orders.totalRevenue().inMillions()}M")
println("\n=== Receita por Cliente ===")
orders.revenueByCustomer().forEach { (customer, revenue) ->
println(" $customer: ${revenue.toUSD()}")
}
// Extensão nullable
val name: String? = null
println("\nNome padrão: ${name.orDefault("Guest")}")
}
Saída:
ORD-001
ORD-001
true
=== Resumos dos Pedidos ===
ORD-001 | $299.99 USD | CONFIRMED | Alice
ORD-002 | $15000.00 USD | PENDING | Bob
ORD-003 | $2500.00 USD | SHIPPED | Charlie
ORD-004 | $8900.00 USD | CONFIRMED | Bob
ORD-005 | $45.50 USD | CANCELLED | Alice
=== Pedidos de Alto Valor ===
ORD-002 | $15000.00 USD | PENDING | Bob
=== Receita ===
Total: $26745.49 USD
Em milhões: 0.02674549M
=== Receita por Cliente ===
Alice: $345.49 USD
Bob: $23900.00 USD
Charlie: $2500.00 USD
Nome padrão: Guest
9. Exemplos práticos rápidos
▶ Exemplo: Extensão de funções em String
// Extensões em tipos padrão
fun String.truncate(maxLength: Int): String =
if (this.length <= maxLength) this
else substring(0, maxLength - 3) + "..."
fun String.maskMiddle(char: Char = '*'): String {
val len = this.length
if (len <= 2) return char.toString().repeat(len)
val mid = len - 2
return "${this[0]}${char.toString().repeat(mid)}${this[len - 1]}"
}
fun String.isEmail(): Boolean = matches(Regex("^[\\w.]+@[\\w.]+\\.\\w+$"))
val text = "ORD-001-Premium-Customer"
println("Truncado: ${text.truncate(12)}")
val email = "alice@example.com"
println("Email mask: ${email.maskMiddle()}")
println("É email? ${email.isEmail()}")
Saída:
Truncado: ORD-001-Pre...
Email mask: a****************m
É email? true
▶ Exemplo: Extensão em classes próprias
data class Order(val id: String, val total: Double, val currency: String = "USD")
// Extensões não modificam o tipo - comportamento adicional definido fora da classe
fun Order.formatted(): String = "$id: \$$total $currency"
fun Order.isHighValue(): Boolean = total > 10_000
fun Order.doubled(): Order = copy(total = total * 2)
val order = Order("ORD-001", 299.99)
println(order.formatted())
println("Alto valor? ${order.isHighValue()}")
println("Dobrado: ${order.doubled().formatted()}")
Saída:
ORD-001: $299.99 USD
Alto valor? false
Dobrado: ORD-001: $599.98 USD
▶ Exemplo: Propriedades de extensão
// Pode adicionar propriedades por meio de getters de extensão
val String.lastChar: Char
get() = this[length - 1]
val String.isPalindrome: Boolean
get() = this == this.reversed()
val String.wordCount: Int
get() = split(Regex("\\s+")).size
println("'Kotlin'.lastChar: ${"Kotlin".lastChar}")
println("'arara'.isPalindrome: ${"arara".isPalindrome}")
println("'hello'.wordCount: ${"hello world kotlin".wordCount}")
Saída:
'Kotlin'.lastChar: n
'arara'.isPalindrome: true
'hello'.wordCount: 3
▶ Exemplo: Extensões em coleções
// Extensões em coleções para semântica específica
fun <T> List<T>.second(): T = this[1]
fun <T> List<T>.secondOrNull(): T? = if (size >= 2) this[1] else null
fun List<Int>.sumPositive(): Int = filter { it > 0 }.sum()
fun <T> List<T>.joinToStringCustom(
separator: String = ", ",
transform: (T) -> String = { it.toString() }
): String = joinToString(separator) { transform(it) }
val numbers = listOf(-3, 1, -5, 8, 2, 7)
println("Soma positivos: ${numbers.sumPositive()}")
val words = listOf("Ana", "Bruno", "Carla")
println("Nomes: ${words.joinToStringCustom(" | ")}")
println("Tamanhos: ${words.joinToStringCustom { it.length.toString() }}")
val empty = listOf<Int>()
println("Vazio.secondOrNull: ${empty.secondOrNull()}")
Saída:
Soma positivos: 18
Nomes: Ana | Bruno | Carla
Tamanhos: 3 | 5 | 5
Vazio.secondOrNull: null
▶ Exemplo: Extensões em tipos genéricos
// Extensão em tipo genérico - funciona para qualquer T
fun <T> T?.toNonNull(default: T): T = this ?: default
val nullName: String? = null
val name = nullName.toNonNull("Anônimo")
println("Nome: $name")
val nullAge: Int? = null
val age = nullAge.toNonNull(0)
println("Idade: $age")
// Restrição de upper bound
inline fun <reified T : Enum<T>> T.describe(): String =
"Enum ${this::class.simpleName}.$name (ordinal=$ordinal)"
enum class Status { PENDING, CONFIRMED, SHIPPED }
println(Status.CONFIRMED.describe())
Saída:
Nome: Anônimo
Idade: 0
Enum Status.CONFIRMED (ordinal=1)
▶ Exemplo: Membros companheiros de extensão
// Extensões podem ser declaradas dentro de classes para escopo
class OrderHelpers {
companion object {
fun generateId(): String = "ORD-${System.currentTimeMillis() % 1_000_000}"
}
}
// Chamadas ficam em escopo dentro de OrderHelpers
val newId = OrderHelpers.generateId()
println("Novo ID: $newId")
// Ou como função regular
println("Novo ID: ${OrderHelpers.Companion.generateId()}")
Saída:
Novo ID: ORD-123456
Novo ID: ORD-789012
▶ Exemplo: Operadores como extensões
// Pode definir operadores sobrecarregados como extensões
data class Vector2D(val x: Double, val y: Double) {
operator fun plus(other: Vector2D) = Vector2D(x + other.x, y + other.y)
operator fun times(scalar: Double) = Vector2D(x * scalar, y * scalar)
}
val v1 = Vector2D(1.0, 2.0)
val v2 = Vector2D(3.0, 4.0)
val sum = v1 + v2
val scaled = sum * 2.0
println("v1 + v2 = ($sum.x, $sum.y)")
println("(v1 + v2) * 2 = ($scaled.x, $scaled.y)")
// get/contains como extensões
operator fun Vector2D.component1() = x
operator fun Vector2D.component2() = y
val (x, y) = scaled
println("Destruturação: x=$x, y=$y")
Saída:
v1 + v2 = (4.0, 6.0)
(v1 + v2) * 2 = (8.0, 12.0)
Destruturação: x=8.0, y=12.0
❓ Perguntas Frequentes
P: Funções de extensão podem acessar membros privados? R: Não. Funções de extensão são definidas fora da classe e só podem acessar membros públicos. Esta é a chave para extensões não quebrarem encapsulamento.
P: O que acontece se uma função de extensão tiver o mesmo nome que um método membro? R: Métodos membros têm prioridade. Se a classe já tem um método com o mesmo nome e assinatura, a função de extensão é ignorada. É por isso que extensões são seguras — elas nunca sobrescrevem acidentalmente um comportamento existente.
P: Funções de extensão podem ser herdadas e sobrescritas? R: Não. Funções de extensão são despachadas estaticamente e não participam do despacho de método virtual. Subclasses não podem sobrescrever funções de extensão da classe pai.
P: Por que propriedades de extensão não podem ter campos de apoio? R: Porque propriedades de extensão não são armazenadas em objetos — são apenas lógica de computação. Se precisar armazenar estado, deve usar outros mecanismos (ex.: associações Map).
P: Extensões de nível superior poluem o namespace? R: Elas requerem import para serem usadas, então não poluem automaticamente. É recomendado agrupar extensões relacionadas no mesmo arquivo e importar conforme necessário.
P: O desempenho de funções de extensão é o mesmo que métodos regulares? R: Quase idêntico. Funções de extensão compilam para chamadas de métodos estáticos, que a JVM facilmente faz inline e otimiza. Zero sobrecarga adicional.
📖 Resumo
- Funções de extensão
fun Type.name()adicionam métodos sem modificar o código-fonte - Propriedades de extensão
val Type.nameadicionam propriedades computadas, campos de apoio não permitidos - Extensões são despachadas estaticamente: o tipo declarado determina a chamada, sem polimorfismo
- Métodos membros têm prioridade sobre extensões de mesmo nome — extensões nunca quebram comportamento existente
- Extensões de nível superior requerem import; extensões membro de classe são visíveis apenas dentro da classe
- Funções de extensão evoluem APIs de
Util.method(obj)paraobj.method()
📝 Exercícios
- Iniciante (⭐): Adicione uma função de extensão
containsPatternaStringque verifica se contém um padrão regex dado. Dica: useRegex.containsMatchIn - Intermediário (⭐⭐): Adicione funções de extensão
toOrderId()eisOrderId()aString. Dica: adicionar prefixo "ORD-" / validar com regex - Avançado (⭐⭐⭐): Verifique o despacho estático de funções de extensão: defina
OrdereBulkOrder, cada um com uma função de extensão de mesmo nome, chame através de uma referência tipada comoOrdere observe o resultado. Dica: o tipo declarado determina a chamada