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á


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

KOTLIN
// 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) para obj.method(), um salto quântico na legibilidade do código.


3. Fundamentos de Funções de Extensão

(1) Sintaxe Básica

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

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

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

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

KOTLIN
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

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

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

KOTLIN
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

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

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

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

TEXT 📖 Somente leitura
Truncado: ORD-001-Pre...
Email mask: a****************m
É email? true

▶ Exemplo: Extensão em classes próprias

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

TEXT 📖 Somente leitura
ORD-001: $299.99 USD
Alto valor? false
Dobrado: ORD-001: $599.98 USD

▶ Exemplo: Propriedades de extensão

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

TEXT 📖 Somente leitura
'Kotlin'.lastChar: n
'arara'.isPalindrome: true
'hello'.wordCount: 3

▶ Exemplo: Extensões em coleções

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

TEXT 📖 Somente leitura
Soma positivos: 18
Nomes: Ana | Bruno | Carla
Tamanhos: 3 | 5 | 5
Vazio.secondOrNull: null

▶ Exemplo: Extensões em tipos genéricos

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

TEXT 📖 Somente leitura
Nome: Anônimo
Idade: 0
Enum Status.CONFIRMED (ordinal=1)

▶ Exemplo: Membros companheiros de extensão

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

TEXT 📖 Somente leitura
Novo ID: ORD-123456
Novo ID: ORD-789012

▶ Exemplo: Operadores como extensões

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

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


📝 Exercícios

  1. Iniciante (⭐): Adicione uma função de extensão containsPattern a String que verifica se contém um padrão regex dado. Dica: use Regex.containsMatchIn
  2. Intermediário (⭐⭐): Adicione funções de extensão toOrderId() e isOrderId() a String. Dica: adicionar prefixo "ORD-" / validar com regex
  3. Avançado (⭐⭐⭐): Verifique o despacho estático de funções de extensão: defina Order e BulkOrder, cada um com uma função de extensão de mesmo nome, chame através de uma referência tipada como Order e observe o resultado. Dica: o tipo declarado determina a chamada

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