Kotlin: Generics do Kotlin Explicados

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

Os generics do Kotlin são mais seguros que os do Java — a variância no local de declaração permite que Charlie decida a covariância no momento da definição em vez de repetir wildcards em cada local de uso.

1. O que Você Aprenderá


2. A História Real de um Arquiteto

(1) Ponto de Dor: O Inferno dos Wildcards de Generics do Java

Charlie escreve List<? extends Order> para listas de pedidos somente leitura e List<? super Order> para listas somente escrita em Java. Cada local de uso requer repetir wildcards, tornando o código quase ilegível.

(2) Solução com Variância no Local de Declaração do Kotlin

JAVA
// Java: repetir wildcard em cada local de uso
void process(List<? extends Order> orders) { }
void addAll(List<? super Order> target) { }

// Kotlin: declarar variância uma vez no local de definição
class Repository<out T : Order> {  // Covariante na definição
    fun getAll(): List<T>  // T só aparece na posição 'out'
}
// Nenhum wildcard necessário no local de uso!

Variância no local de declaração: defina uma vez, use infinitamente. Variância no local de uso do Java: repita em cada local de chamada.


3. Fundamentos de Generics

(1) Classes e Funções Genéricas

KOTLIN
// Classe genérica
class Box<T>(val value: T) {
    fun unwrap(): T = value
}

val intBox = Box(42)         // T inferido como Int
val strBox = Box("Hello")   // T inferido como String

// Função genérica
fun <T> singletonList(item: T): List<T> = listOf(item)

// Genérico com restrição de tipo
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b

// Múltiplas restrições
fun <T> process(item: T) where T : CharSequence, T : Comparable<T> {
    // T é tanto CharSequence quanto Comparable
}

(2) O Problema da Variância: Por Que Precisamos de out/in

KOTLIN
// Isso parece razoável, mas é um ERRO DE TIPO
val strings: List<String> = listOf("Hello", "World")
// val objects: List<Any> = strings  // ERRO no Kotlin (OK em arrays Java)

// Por quê? Se permitido:
// objects.add(42)  // Corromperia a lista de Strings!
// strings[0].length  // ClassCastException em tempo de execução

4. Variância no Local de Declaração

(1) out (Covariância): Somente Produtor, Nunca Consumidor

KOTLIN
// Produtor: T só aparece na posição 'out' (tipo de retorno)
interface Repository<out T> {
    fun findById(id: String): T
    fun findAll(): List<T>
    // fun save(item: T)  // ERRO: T na posição 'in'
}

// Agora isso funciona:
val orderRepo: Repository<Order> = OrderRepositoryImpl()
val anyRepo: Repository<Any> = orderRepo  // OK! Covariante

(2) in (Contravariância): Somente Consumidor, Nunca Produtor

KOTLIN
// Consumidor: T só aparece na posição 'in' (tipo de parâmetro)
interface Filter<in T> {
    fun test(item: T): Boolean
    fun filter(items: List<T>): List<T>
    // fun getResult(): T  // ERRO: T na posição 'out'
}

// Agora isso funciona:
val orderFilter: Filter<Order> = object : Filter<Order> {
    override fun test(item: Order) = item.total > 1_000
    override fun filter(items: List<Order>) = items.filter { test(it) }
}
val anyFilter: Filter<Any> = orderFilter  // Não pode atribuir - direção contravariante

(3) Regras de Variância em Resumo

Modificador Significado T Aparece Em Direção de Subtipo Análogo
out Covariante Apenas na posição de retorno Producer<Sub> é subtipo de Producer<Base> Java ? extends
in Contravariante Apenas na posição de parâmetro Consumer<Base> é subtipo de Consumer<Sub> Java ? super
Nenhum Invariante Qualquer posição Sem relação de subtipo Java T

(4) Diagrama de Fluxo de Variância

100%
flowchart TD
    subgraph "Covariante (out)"
        A1[Produtor de Sub] -->|"É-UM"| A2[Produtor de Base]
    end
    subgraph "Contravariante (in)"
        B1[Consumidor de Base] -->|"É-UM"| B2[Consumidor de Sub]
    end
    subgraph "Invariante"
        C1[Box de Sub] -.->|"NÃO É-UM"| C2[Box de Base]
    end

5. Variância no Local de Uso (Projeção de Tipo)

Quando você não pode decidir a variância no local de declaração, projete-a no local de uso.

KOTLIN
// Projeção no local de uso: quando o local de declaração é invariante
fun copyFrom(source: Array<out Any>, dest: Array<in Any>) {
    for (i in source.indices) {
        dest[i] = source[i]
    }
}

// Equivalente em Java:
// void copyFrom(Object[] source, Object[] dest)

(1) Comparação de Variância no Local de Uso

Cenário Java Kotlin
Parâmetro somente leitura <? extends T> <out T>
Parâmetro somente escrita <? super T> <in T>
Declaração covariante Não suportado out T
Declaração contravariante Não suportado in T

6. Projeção com Estrela

Quando o tipo genérico é desconhecido, use projeção com estrela para acesso seguro.

KOTLIN
// List<*> = lista de tipo desconhecido (seguro para ler, não para escrever)
val unknownList: List<*> = listOf("Hello", 42, 3.14)
val first: Any? = unknownList[0]  // OK - retorna Any?
// unknownList.add("New")  // ERRO - não pode escrever

// Comparação
fun printSize(list: List<*>) {
    println("Tamanho: ${list.size}")  // OK - size não depende de T
}

// Array<*> é diferente
val array: Array<*> = arrayOf("A", "B")
// array[0] = "C"  // ERRO - não pode escrever em Array<*>
val elem: Any? = array[0]  // OK - pode ler

(1) Regras da Projeção com Estrela

Tipo Equivalente à Projeção com Estrela Legível Gravável
Foo<out T> Foo<out Any?> ✅ (Any?)
Foo<in T> Foo<in Nothing> ✅ (Nothing = não gravável)
Foo<T> (invariante, T limite superior) Foo<out Any?> ✅ (Any?)
Foo<T> (T limite superior T : Upper) Foo<out Upper> ✅ (Upper)

7. Parâmetros de Tipo reified

(1) Apagamento de Tipo e reified

KOTLIN
// Genérico normal: tipo é apagado em tempo de execução
fun <T> isA(value: Any): Boolean = value is T  // ERRO: Não é possível verificar instância de tipo apagado

// reified: função inline preserva o tipo no local de chamada
inline fun <reified T> isA(value: Any): Boolean = value is T  // OK!

// Uso
isA<String>("Hello")  // true
isA<Int>(42)          // true
isA<Int>("42")        // false

// Prático: filtrar por tipo
inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
    return filter { it is T }.map { it as T }
}

(2) Casos de Uso Práticos de reified

KOTLIN
// Desserialização JSON sem parâmetro Class
inline fun <reified T> Json.decodeFromString(json: String): T

// Em vez de:
// json.decodeFromString(Order::class.java, jsonString)

// Apenas:
// json.decodeFromString<Order>(jsonString)

8. Exemplo Completo: Repositório de Pedidos Type-Safe

KOTLIN
// ============================================
// OrderProcessor - Repositório Type-Safe com Generics
// Recurso: Repositório genérico com variância
// ============================================

open class Order(val id: String, val total: Double)
class BulkOrder(id: String, total: Double, val minQty: Int) : Order(id, total)
class PriorityOrder(id: String, total: Double, val priority: String) : Order(id, total)

// Repositório covariante: somente produz T
interface ReadOnlyRepository<out T : Order> {
    fun findById(id: String): T?
    fun findAll(): List<T>
}

// Escritor contravariante: somente consome T
interface WriteRepository<in T : Order> {
    fun save(item: T)
    fun saveAll(items: List<T>)
}

// Repositório completo: invariante (lê e escreve)
interface OrderRepository<T : Order> : ReadOnlyRepository<T>, WriteRepository<T>

// Implementação em memória
class InMemoryOrderRepository<T : Order> : OrderRepository<T> {
    private val storage = mutableMapOf<String, T>()

    override fun findById(id: String): T? = storage[id]
    override fun findAll(): List<T> = storage.values.toList()
    override fun save(item: T) { storage[item.id] = item }
    override fun saveAll(items: List<T>) { items.forEach { save(it) } }
}

// Filtro genérico com contravariância
class OrderFilter<in T : Order>(private val predicate: (T) -> Boolean) {
    fun test(item: T): Boolean = predicate(item)
    fun filter(items: List<T>): List<T> = items.filter(predicate)
}

inline fun <reified T : Order> List<Order>.filterByType(): List<T> {
    return this.filterIsInstance<T>()
}

fun main() {
    val orders = listOf(
        Order("ORD-001", 299.99),
        BulkOrder("ORD-002", 5_000.00, 100),
        PriorityOrder("ORD-003", 15_000.00, "VIP"),
        BulkOrder("ORD-004", 8_000.00, 50),
        PriorityOrder("ORD-005", 2_500.00, "GOLD")
    )

    // Covariante: repo de BulkOrder atribuível a repo de Order
    val bulkRepo: ReadOnlyRepository<BulkOrder> = InMemoryOrderRepository()
    val orderRepo: ReadOnlyRepository<Order> = bulkRepo  // OK: covariante

    // reified: filtrar por tipo em tempo de execução
    val bulkOrders: List<BulkOrder> = orders.filterByType<BulkOrder>()
    println("Pedidos em atacado: ${bulkOrders.map { it.id }}")

    val priorityOrders: List<PriorityOrder> = orders.filterByType<PriorityOrder>()
    println("Pedidos prioritários: ${priorityOrders.map { it.id }}")

    // Filtro genérico
    val highValueFilter = OrderFilter<Order> { it.total > 1_000 }
    val highValue = highValueFilter.filter(orders)
    println("Pedidos de alto valor: ${highValue.map { it.id }}")
}

Saída:

TEXT 📖 Somente leitura
Pedidos em atacado: [ORD-002, ORD-004]
Pedidos prioritários: [ORD-003, ORD-005]
Pedidos de alto valor: [ORD-002, ORD-003, ORD-004, ORD-005]

9. Exemplos práticos rápidos

▶ Exemplo: Função genérica

KOTLIN
// Função genérica com tipo parametrizado
fun <T> identity(value: T): T = value

println(identity(42))
println(identity("Olá"))
println(identity(listOf(1, 2, 3)))

// Inferência de tipo
val order: String = identity("ORD-001")
println("Tipo da ordem: ${order::class.simpleName}")

// Restrição de tipo superior: T deve ser Comparable<T>
fun <T : Comparable<T>> maxOf(a: T, b: T): T = if (a > b) a else b

println(maxOf(10, 20))
println(maxOf("Alice", "Bob"))  // ordem alfabética

Saída:

TEXT 📖 Somente leitura
42
Olá
[1, 2, 3]
Tipo da ordem: String
20
Bob

▶ Exemplo: Classes genéricas

KOTLIN
// Container genérico - funciona com qualquer tipo
class Box<T>(private val content: T) {
    fun get(): T = content
    fun describe(): String = "Box<${content!!::class.simpleName}>: $content"
}

val intBox = Box(42)
val strBox = Box("ORD-001")
val listBox = Box(listOf(1, 2, 3))

println(intBox.describe())
println(strBox.describe())
println(listBox.describe())

// Tipo em construtor + métodos
class Container<T>(var item: T) {
    fun replace(newItem: T) {
        println("Substituindo $item por $newItem")
        item = newItem
    }
}

val container = Container("init")
container.replace("updated")
println("Atual: ${container.item}")

Saída:

TEXT 📖 Somente leitura
Box<Int>: 42
Box<String>: ORD-001
Box<List>: [1, 2, 3]
Substituindo init por updated
Atual: updated

▶ Exemplo: Variação - out (covariante)

KOTLIN
// out T: T é apenas produzido (saída), não consumido
// Permite passar List<Subtype> onde List<Supertype> é esperado
interface Producer<out T> {
    fun produce(): T
}

class StringProducer : Producer<String> {
    override fun produce(): String = "Hello"
}

// Producer<String> pode ser usado onde Producer<Any> é esperado
val producer: Producer<Any> = StringProducer()
println(producer.produce())

Saída:

TEXT 📖 Somente leitura
Hello

▶ Exemplo: Variação - in (contravariante)

KOTLIN
// in T: T é apenas consumido (entrada), não produzido
// Permite passar Comparator<Supertype> onde Comparator<Subtype> é esperado
interface Consumer<in T> {
    fun consume(item: T)
}

class AnyConsumer : Consumer<Any> {
    override fun consume(item: Any) = println("Consumido: $item")
}

// AnyConsumer pode ser usado onde Consumer<String> é esperado
val stringConsumer: Consumer<String> = AnyConsumer()
stringConsumer.consume("ORD-001")
stringConsumer.consume("ORD-002")

Saída:

TEXT 📖 Somente leitura
Consumido: ORD-001
Consumido: ORD-002

▶ Exemplo: Star projection

KOTLIN
// *: usado quando o tipo exato é desconhecido ou irrelevante
// MutableList<*> permite ler elementos como Any?, mas não gravar tipos específicos
val list: MutableList<Any?> = mutableListOf(1, "Alice", 3.14, null)

fun firstElement(items: List<*>): Any? = items.firstOrNull()

println(firstElement(list))
println(firstElement(listOf("a", "b", "c")))

// Cópia segura - unknown para outro container
val source: List<*> = listOf(1, "x", true)
val copy: List<Any?> = source.toList()
println("Cópia: $copy")

Saída:

TEXT 📖 Somente leitura
1
a
Cópia: [1, x, true]

▶ Exemplo: Type constraints

KOTLIN
// Where clause para múltiplos constraints
fun <T> describe(value: T): String where T : Number, T : Comparable<T> {
    return "Valor: $value, class: ${value::class.simpleName}, positivo: ${value >= 0}"
}

println(describe(42))
println(describe(3.14))
println(describe(-1))  // negativo

// Upper bound: tipo deve ser subclasse
fun <T : CharSequence> longest(a: T, b: T): T = if (a.length >= b.length) a else b

println(longest("hello", "hi"))
println(longest("kotlin", "java"))

Saída:

TEXT 📖 Somente leitura
Valor: 42, class: Int, positivo: true
Valor: 3.14, class: Double, positivo: true
Valor: -1, class: Int, positivo: false
hello
kotlin

▶ Exemplo: Reified type parameters

KOTLIN
// reified: tipo genérico disponível em tempo de execução (em funções inline)
inline fun <reified T> List<*>.countOf(): Int = count { it is T }

val mixed = listOf("Alice", 42, "Bob", 3.14, "Charlie", true)
println("Strings: ${mixed.countOf<String>()}")
println("Ints: ${mixed.countOf<Int>()}")
println("Doubles: ${mixed.countOf<Double>()}")

// Filtrar por tipo
inline fun <reified T> List<*>.filterByType(): List<T> = filterIsInstance<T>()
println("Strings filtradas: ${mixed.filterByType<String>()}")

Saída:

TEXT 📖 Somente leitura
Strings: 2
Ints: 1
Doubles: 1
Strings filtradas: [Alice, Bob, Charlie]

❓ Perguntas Frequentes

P: out e in podem ser usados no mesmo parâmetro de tipo simultaneamente? R: Não. Um parâmetro de tipo só pode ser out, in ou invariante. Se T aparece tanto na posição de entrada quanto de saída, deve ser invariante.

P: Os generics do Kotlin ainda existem em tempo de execução? R: Maior parte apagada (igual ao Java). Mas inline + reified pode preservar informações de tipo. Funções inline são expandidas no local de chamada, então a informação de tipo está disponível em tempo de compilação.

P: Qual a diferença entre projeção com estrela * e Any? R: List<*> significa "tipo de elemento desconhecido", legível apenas como Any?; List<Any> significa "tipo de elemento é Any", você pode adicionar qualquer valor. O primeiro é mais seguro.

P: Quando usar variância no local de declaração vs local de uso? R: Prefira variância no local de declaração — decida no momento do design da API se T é somente leitura (out) ou somente escrita (in). Variância no local de uso é o último recurso.

P: reified tem sobrecarga de desempenho? R: Nenhuma sobrecarga adicional em tempo de execução. Como funções inline são expandidas em tempo de compilação, verificações de tipo reified acontecem em tempo de compilação sem trabalho extra em tempo de execução.

P: Qual a diferença entre <T : Upper> e where T : Upper? R: Restrição única usa a abreviação <T : Upper>; múltiplas restrições devem usar a sintaxe where.


📖 Resumo


📝 Exercícios

  1. Iniciante (⭐): Implemente um Box<T> genérico e teste compatibilidade de atribuição com diferentes argumentos de tipo. Dica: Box<Int> pode ser atribuído a Box<Any>?
  2. Intermediário (⭐⭐): Defina ReadOnlyRepository<out T> e WriteRepository<in T>, demonstre relações de atribuição de covariância e contravariância. Dica: List<BulkOrder>List<out Order>
  3. Avançado (⭐⭐⭐): Implemente uma função spotNull<reified Any> que filtra por tipo e depois faz cast. Dica: use filterIsInstance<T>

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