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á
- Variância:
out(covariância) vsin(contravariância) - Variância no local de uso: projeção de tipo
- Projeção com estrela:
*acesso seguro a generics desconhecidos reified: funções inline preservam informações de tipo genérico- Charlie em ação:
ReadOnlyRepository<out T>/OrderFilter<in T>
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: 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
// 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
// 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
// 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
// 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
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.
// 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.
// 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
// 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
// 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
// ============================================
// 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:
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
// 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:
42
Olá
[1, 2, 3]
Tipo da ordem: String
20
Bob
▶ Exemplo: Classes genéricas
// 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:
Box<Int>: 42
Box<String>: ORD-001
Box<List>: [1, 2, 3]
Substituindo init por updated
Atual: updated
▶ Exemplo: Variação - out (covariante)
// 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:
Hello
▶ Exemplo: Variação - in (contravariante)
// 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:
Consumido: ORD-001
Consumido: ORD-002
▶ Exemplo: Star projection
// *: 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:
1
a
Cópia: [1, x, true]
▶ Exemplo: Type constraints
// 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:
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
// 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:
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
*eAny? 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>ewhere T : Upper? R: Restrição única usa a abreviação<T : Upper>; múltiplas restrições devem usar a sintaxewhere.
📖 Resumo
out(covariância): T só aparece na posição de retorno,Producer<Sub>é subtipo deProducer<Base>in(contravariância): T só aparece na posição de parâmetro,Consumer<Base>é subtipo deConsumer<Sub>- Variância no local de declaração define uma vez; variância no local de uso (projeção) é uma substituição sob demanda
- Projeção com estrela
*é para cenários de tipo genérico desconhecido — seguro, mas limitado reifiedpreserva informações de tipo genérico em funções inline, permitindo verificações de tipo em tempo de execução- Mnemônico de variância: Produtor → out, Consumidor → in
📝 Exercícios
- 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 aBox<Any>? - Intermediário (⭐⭐): Defina
ReadOnlyRepository<out T>eWriteRepository<in T>, demonstre relações de atribuição de covariância e contravariância. Dica:List<BulkOrder>→List<out Order> - Avançado (⭐⭐⭐): Implemente uma função
spotNull<reified Any>que filtra por tipo e depois faz cast. Dica: usefilterIsInstance<T>