O que são Generics em Kotlin?

Generics permitem criar classes, interfaces e funções que funcionam com qualquer tipo, mantendo a segurança de tipos em tempo de compilação. Em vez de escrever código específico para Int, String ou qualquer outro tipo, você escreve uma vez e reutiliza com qualquer um deles.

Classe genérica

class Caixa<T>(val conteudo: T) {
    fun abrir(): T {
        println("Abrindo a caixa...")
        return conteudo
    }
}

fun main() {
    val caixaDeTexto = Caixa("Presente surpresa")
    val caixaDeNumero = Caixa(42)

    println(caixaDeTexto.abrir()) // Presente surpresa
    println(caixaDeNumero.abrir()) // 42
}

O T é um parâmetro de tipo — pode ser qualquer coisa. O compilador garante que você não misture os tipos.

Função genérica

fun <T> trocar(par: Pair<T, T>): Pair<T, T> {
    return Pair(par.second, par.first)
}

fun main() {
    val original = Pair("Kotlin", "Java")
    val trocado = trocar(original)
    println(trocado) // (Java, Kotlin)
}

Restrições de tipo

Você pode restringir quais tipos são aceitos usando where ou ::

fun <T : Comparable<T>> maior(a: T, b: T): T {
    return if (a > b) a else b
}

fun main() {
    println(maior(10, 20))         // 20
    println(maior("Kotlin", "Java")) // Kotlin
}

Quando precisa de múltiplas restrições, use a cláusula where:

fun <T> processar(item: T) where T : Comparable<T>, T : CharSequence {
    println("Tamanho: ${item.length}, Valor: $item")
}

fun main() {
    processar("Kotlin") // Tamanho: 6, Valor: Kotlin
    // processar(42) // Erro: Int nao implementa CharSequence
}

Variância: in e out

Kotlin tem um sistema de variância mais explícito que Java:

// out = produtor (covariant) — só retorna T
interface Fonte<out T> {
    fun obter(): T
}

// in = consumidor (contravariant) — só recebe T
interface Destino<in T> {
    fun enviar(item: T)
}

class FonteDeString : Fonte<String> {
    override fun obter() = "Kotlin Brasil"
}

fun main() {
    val fonte: Fonte<Any> = FonteDeString() // Funciona por causa do out
    println(fonte.obter())
}
  • out T significa que T só aparece como retorno (posição de saída)
  • in T significa que T só aparece como parâmetro (posição de entrada)

Projeção de tipo (star projection)

Quando você não se importa com o tipo genérico, use *:

fun imprimirItens(lista: List<*>) {
    lista.forEach { println(it) }
}

Reified type parameters

Em Kotlin, os tipos genéricos são apagados em tempo de execução (type erasure). Porém, com funções inline e o modificador reified, você pode acessar o tipo genérico em runtime:

inline fun <reified T> verificarTipo(valor: Any): Boolean {
    return valor is T
}

fun main() {
    println(verificarTipo<String>("Kotlin")) // true
    println(verificarTipo<Int>("Kotlin"))    // false
}

Isso é muito usado em frameworks para deserialização, injeção de dependência e navegação entre telas no Android.

Generics em interfaces e herança

Generics se combinam naturalmente com interfaces para criar contratos flexíveis:

interface Repositorio<T> {
    fun buscarPorId(id: Int): T?
    fun salvar(item: T)
    fun listarTodos(): List<T>
}

data class Produto(val id: Int, val nome: String, val preco: Double)

class ProdutoRepositorio : Repositorio<Produto> {
    private val itens = mutableListOf<Produto>()

    override fun buscarPorId(id: Int) = itens.find { it.id == id }
    override fun salvar(item: Produto) { itens.add(item) }
    override fun listarTodos() = itens.toList()
}

fun main() {
    val repo = ProdutoRepositorio()
    repo.salvar(Produto(1, "Notebook", 3500.0))
    repo.salvar(Produto(2, "Mouse", 89.90))
    println(repo.listarTodos())
}

Casos de Uso no Mundo Real

  • Repositórios genéricos: criar uma interface Repositorio<T> que define operações CRUD e implementar para cada entidade do sistema, eliminando duplicação de código de acesso a dados.
  • Respostas de API padronizadas: usar uma classe ApiResponse<T> que encapsula sucesso, erro e loading, permitindo que qualquer endpoint retorne o mesmo formato independente do tipo de dado.
  • Adapters no Android: RecyclerView.Adapter<VH> usa generics para tipar o ViewHolder, e você pode criar adapters genéricos que funcionam com diferentes tipos de itens de lista.
  • Injeção de dependência: frameworks como Koin e Hilt usam reified type parameters para resolver dependências pelo tipo sem precisar passar a classe como parâmetro explícito.

Boas Práticas

  • Use nomes de parâmetros de tipo descritivos quando o contexto exigir: T para tipo genérico, K e V para chave e valor, E para elementos de coleção, R para tipo de retorno.
  • Prefira out em interfaces que apenas produzem valores e in em interfaces que apenas consomem. Isso torna a API mais flexível para os consumidores.
  • Utilize restrições de tipo (T : AlgumaInterface) sempre que possível para evitar casts desnecessários e manter a segurança de tipos.
  • Combine inline com reified quando precisar acessar o tipo genérico em runtime, em vez de passar KClass<T> como parâmetro.
  • Evite star projection (*) quando você pode ser mais específico sobre o tipo, pois * limita as operações disponíveis.

Erros Comuns

  • Tentar fazer is T sem reified: devido ao type erasure, verificar o tipo genérico em runtime exige que a função seja inline e o parâmetro de tipo seja reified.
  • Confundir in e out: usar out quando o tipo aparece em posição de entrada (parâmetro) causa erro de compilação. Lembre: out = saída (retorno), in = entrada (parâmetro).
  • Criar hierarquias genéricas excessivamente complexas: aninhar múltiplos parâmetros de tipo como Classe<A<B<C>>> dificulta a leitura. Simplifique com typealias ou quebre em interfaces menores.
  • Esquecer que List<Any> não é supertipo de List<String> sem variância: em Kotlin, List já é declarada como List<out E>, então funciona. Mas para suas próprias classes, você precisa declarar a variância explicitamente.
  • Usar casts genéricos não verificados: lista as List<String> gera um warning de unchecked cast porque o tipo genérico é apagado em runtime. Use filterIsInstance<String>() quando possível.

Perguntas Frequentes

Qual a diferença entre generics em Kotlin e em Java? Kotlin usa in e out para variância no local de declaração (declaration-site variance), enquanto Java usa wildcards ? extends e ? super no local de uso (use-site variance). Kotlin também suporta reified type parameters, que não existem em Java.

O que é type erasure e como afeta generics? Type erasure significa que os tipos genéricos são apagados em tempo de execução. Um List<String> e um List<Int> são indistinguíveis na JVM. O modificador reified em funções inline é a solução do Kotlin para contornar essa limitação.

Posso ter múltiplos parâmetros de tipo? Sim. Você pode declarar quantos precisar: class Mapa<K, V>, fun <A, B> converter(valor: A, transformador: (A) -> B): B. Cada parâmetro pode ter suas próprias restrições.

Quando devo usar star projection? Use * quando você precisa trabalhar com um tipo genérico mas não se importa com o parâmetro de tipo específico, como em funções que apenas imprimem ou verificam se uma coleção está vazia.

Termos Relacionados

  • Interface — contratos que frequentemente utilizam generics para definir APIs flexíveis
  • Inline — modificador necessário para usar reified type parameters
  • Fun — funções genéricas são declaradas com fun seguido de parâmetros de tipo
  • Lambda — frequentemente usada com funções genéricas como map<T, R>

Generics são essenciais para escrever código reutilizável e type-safe. A biblioteca padrão do Kotlin usa generics em praticamente tudo: listas, maps, flows e muito mais.