Neste tutorial, você vai aprender tudo sobre Generics em Kotlin — um recurso fundamental para escrever código reutilizável e type-safe. Generics permitem criar classes, interfaces e funções que operam com diferentes tipos sem sacrificar a segurança de tipos do compilador. Ao final, você vai dominar type parameters, constraints, variance com in/out, star projection e reified type parameters.
O que São Generics?
Generics permitem que você escreva código que funciona com qualquer tipo, enquanto ainda mantém a verificação de tipos em tempo de compilação. Sem generics, você teria que usar Any e fazer casts manuais, perdendo a segurança de tipos.
Você já usa generics diariamente, mesmo sem perceber. Toda vez que declara uma List<String>, Map<String, Int> ou MutableList<Usuario>, está usando generics.
Type Parameters Básicos
A sintaxe básica de generics usa colchetes angulares <T>, onde T é o type parameter — um “placeholder” para o tipo real que será usado:
// Classe genérica
class Caixa<T>(val conteudo: T) {
fun abrir(): T = conteudo
fun transformar(bloco: (T) -> String): String {
return bloco(conteudo)
}
}
fun main() {
val caixaTexto = Caixa("Kotlin Brasil")
println(caixaTexto.abrir()) // Kotlin Brasil
val caixaNumero = Caixa(42)
println(caixaNumero.abrir()) // 42
// O tipo é inferido automaticamente
val caixaLista = Caixa(listOf(1, 2, 3))
println(caixaLista.abrir()) // [1, 2, 3]
// Transformando o conteúdo
println(caixaNumero.transformar { "O número é $it" })
}
Note que o compilador infere o tipo genérico automaticamente na maioria dos casos. Você não precisa escrever Caixa<String>("Kotlin Brasil") — o compilador deduz String a partir do argumento.
Funções Genéricas
Funções também podem ser genéricas, independentemente de estarem dentro de uma classe genérica ou não:
fun <T> primeiroOuNull(lista: List<T>): T? {
return if (lista.isNotEmpty()) lista[0] else null
}
fun <T> List<T>.trocar(indice1: Int, indice2: Int): List<T> {
val resultado = this.toMutableList()
val temp = resultado[indice1]
resultado[indice1] = resultado[indice2]
resultado[indice2] = temp
return resultado
}
fun <K, V> mapearPares(pares: List<Pair<K, V>>): Map<K, V> {
return pares.toMap()
}
fun main() {
println(primeiroOuNull(listOf("a", "b", "c"))) // a
println(primeiroOuNull(emptyList<Int>())) // null
val numeros = listOf(10, 20, 30, 40)
println(numeros.trocar(0, 3)) // [40, 20, 30, 10]
val mapa = mapearPares(listOf("nome" to "Ana", "cidade" to "SP"))
println(mapa) // {nome=Ana, cidade=SP}
}
Constraints (Upper Bounds)
Às vezes você quer restringir o tipo genérico para aceitar apenas subtipos de uma determinada classe ou interface. Isso é feito com upper bounds:
// T precisa ser Comparable
fun <T : Comparable<T>> maximo(a: T, b: T): T {
return if (a > b) a else b
}
// T precisa implementar CharSequence
fun <T : CharSequence> contarCaracteres(texto: T): Int {
return texto.length
}
fun main() {
println(maximo(10, 20)) // 20
println(maximo("abacaxi", "banana")) // banana
println(contarCaracteres("Kotlin")) // 6
// Isto não compila:
// maximo(listOf(1), listOf(2)) — List não é Comparable
}
Para múltiplas restrições, use a cláusula where:
fun <T> processar(item: T) where T : Comparable<T>, T : CharSequence {
println("Valor: $item, Tamanho: ${item.length}")
// T é tanto Comparable quanto CharSequence
}
fun main() {
processar("Kotlin") // String implementa ambas
// processar(42) — Int não implementa CharSequence — ERRO!
}
Variance: in e out
Variance é um dos conceitos mais importantes (e confusos) de generics. Ela define como a relação de herança entre tipos afeta tipos genéricos.
Covariância com out (Producer)
Se Dog é subtipo de Animal, uma List<Dog> deveria ser subtipo de List<Animal>? Em Kotlin, sim — porque List é declarada com out:
// out = covariante = o tipo só SAI (é produzido)
interface Produtor<out T> {
fun produzir(): T
// fun consumir(item: T) — ERRO! Não pode usar T como parâmetro
}
class ProdutorDeString : Produtor<String> {
override fun produzir(): String = "Hello Kotlin"
}
fun imprimirProduto(produtor: Produtor<Any>) {
println(produtor.produzir())
}
fun main() {
val produtorString: Produtor<String> = ProdutorDeString()
// Funciona! Produtor<String> é subtipo de Produtor<Any>
imprimirProduto(produtorString)
}
Use out quando o tipo genérico é apenas produzido (retornado), nunca consumido como parâmetro.
Contravariância com in (Consumer)
O oposto: se Dog é subtipo de Animal, um Comparator<Animal> pode ser usado onde se espera Comparator<Dog>:
// in = contravariante = o tipo só ENTRA (é consumido)
interface Consumidor<in T> {
fun consumir(item: T)
// fun produzir(): T — ERRO! Não pode retornar T
}
class ConsumidorDeAny : Consumidor<Any> {
override fun consumir(item: Any) {
println("Consumindo: $item")
}
}
fun alimentar(consumidor: Consumidor<String>) {
consumidor.consumir("Kotlin")
}
fun main() {
val consumidorAny: Consumidor<Any> = ConsumidorDeAny()
// Funciona! Consumidor<Any> é subtipo de Consumidor<String>
alimentar(consumidorAny)
}
Resumo de Variance
// Pense assim:
// out T → Producer<out T> → T só aparece em posição de retorno
// in T → Consumer<in T> → T só aparece em posição de parâmetro
// Exemplos da stdlib:
// List<out E> → só produz E (get, iterator)
// Comparable<in T> → só consome T (compareTo recebe T)
// MutableList<E> → invariante (produz E e consome E)
Star Projection
Quando você não sabe ou não se importa com o tipo genérico, use a star projection *:
fun imprimirConteudo(lista: List<*>) {
for (item in lista) {
println(item) // item é do tipo Any?
}
}
fun verificarTipo(caixa: Caixa<*>) {
val conteudo = caixa.abrir() // tipo: Any?
println("Conteúdo: $conteudo")
}
fun main() {
imprimirConteudo(listOf(1, "dois", 3.0, true))
val caixas: List<Caixa<*>> = listOf(
Caixa("texto"),
Caixa(42),
Caixa(true)
)
caixas.forEach { verificarTipo(it) }
}
List<*> é equivalente a List<out Any?> — você pode ler itens como Any?, mas não pode adicionar itens (exceto null).
Reified Type Parameters
Devido ao type erasure da JVM, informações sobre generics são apagadas em tempo de execução. Isso significa que você não pode fazer is T ou T::class normalmente. A solução é usar reified com funções inline:
inline fun <reified T> verificarTipo(valor: Any): Boolean {
return valor is T
}
inline fun <reified T> filtrarPorTipo(lista: List<Any>): List<T> {
return lista.filterIsInstance<T>()
}
inline fun <reified T> nomeDoTipo(): String {
return T::class.simpleName ?: "Desconhecido"
}
fun main() {
println(verificarTipo<String>("texto")) // true
println(verificarTipo<Int>("texto")) // false
val misturada = listOf(1, "dois", 3, "quatro", 5.0)
val strings = filtrarPorTipo<String>(misturada)
println(strings) // [dois, quatro]
val inteiros = filtrarPorTipo<Int>(misturada)
println(inteiros) // [1, 3]
println(nomeDoTipo<String>()) // String
println(nomeDoTipo<List<Int>>()) // List
}
reified só funciona com funções inline porque o compilador substitui a chamada da função pelo seu corpo, inserindo o tipo real no lugar do type parameter.
Type Erasure
Na JVM, informações de tipo genérico são apagadas durante a compilação. Isso tem implicações práticas:
fun main() {
val listaStrings = listOf("a", "b")
val listaInts = listOf(1, 2)
// Em tempo de execução, ambas são apenas "List"
// Isto não funciona:
// if (listaStrings is List<String>) — ERRO: Cannot check for instance of erased type
// Mas isto funciona com star projection:
if (listaStrings is List<*>) {
println("É uma lista!") // Funciona
}
// Para verificar o tipo dos elementos, use reified:
}
// Abordagem segura com reified
inline fun <reified T> List<*>.ehListaDe(): Boolean {
return all { it is T }
}
Exemplo Prático Completo
Vamos criar um repositório genérico que funciona com qualquer entidade:
interface Entidade {
val id: Long
}
data class Usuario(override val id: Long, val nome: String, val email: String) : Entidade
data class Produto(override val id: Long, val nome: String, val preco: Double) : Entidade
class Repositorio<T : Entidade> {
private val itens = mutableMapOf<Long, T>()
fun salvar(item: T): T {
itens[item.id] = item
return item
}
fun buscarPorId(id: Long): T? = itens[id]
fun listarTodos(): List<T> = itens.values.toList()
fun remover(id: Long): Boolean = itens.remove(id) != null
fun buscar(filtro: (T) -> Boolean): List<T> = itens.values.filter(filtro)
val tamanho: Int get() = itens.size
}
fun main() {
val usuarios = Repositorio<Usuario>()
usuarios.salvar(Usuario(1, "Ana", "ana@email.com"))
usuarios.salvar(Usuario(2, "Bruno", "bruno@email.com"))
usuarios.salvar(Usuario(3, "Carla", "carla@email.com"))
println("Total: ${usuarios.tamanho}")
val encontrado = usuarios.buscarPorId(2)
println("Encontrado: $encontrado")
val comA = usuarios.buscar { it.nome.startsWith("A") || it.nome.startsWith("a") }
println("Nomes com A: $comA")
val produtos = Repositorio<Produto>()
produtos.salvar(Produto(1, "Notebook", 3500.0))
produtos.salvar(Produto(2, "Mouse", 89.90))
val caros = produtos.buscar { it.preco > 100 }
println("Produtos caros: $caros")
}
Erros Comuns
Esquecer type erasure: tentar fazer
is List<String>em tempo de execução não funciona. Use star projection (is List<*>) oureifiedcom funçõesinline.Confundir
ineout: lembre da regra mnemônica — out = Producer (o tipo sai), in = Consumer (o tipo entra). Se errar a variance, o compilador mostrará erros confusos sobre posições ilegais.Usar
reifiedseminline:reifiedsó funciona com funçõesinline. Sem inline, o tipo seria apagado ereifiednão teria efeito. O compilador não permitirá essa combinação inválida.Não definir upper bounds quando necessário: se você chama métodos específicos de um tipo dentro de uma função genérica (como
compareTo), precisa declarar o bound (<T : Comparable<T>>). Caso contrário,Té tratado comoAny?e o método não estará disponível.Ignorar a nullability de type parameters: por padrão,
<T>pode ser nullable (TaceitaString?). Se você precisa garantir que T nunca será null, use<T : Any>como upper bound.
Conclusão e Próximos Passos
Generics são essenciais para escrever código Kotlin robusto e reutilizável. Neste tutorial, você aprendeu type parameters, constraints com upper bounds, variance com in/out, star projection, reified type parameters e type erasure. Esses conceitos formam a base para entender APIs complexas como as de coleções, coroutines e frameworks como Ktor e Spring.
Para continuar aprofundando, explore:
- Sealed Classes para hierarquias de tipos genéricos
- Extension Functions genéricas
- Lambdas com tipos genéricos em higher-order functions
- Coroutines que usam generics extensivamente (
Deferred<T>,Flow<T>)
Pratique criando suas próprias classes e funções genéricas. Comece simples e vá adicionando constraints e variance conforme a necessidade. Com o tempo, generics se tornarão naturais no seu código Kotlin.