Introdução: por que generics não são assustadores, mas sim convenientes
\n\nImagine que você está escrevendo uma classe "Caixa". Nela você pode colocar qualquer coisa: um livro, uma maçã, um telefone. Mas se você a fizer para Object, toda vez que for retirar algo terá que fazer as? Book e torcer para não cair em um ClassCastException. Familiar?
É aqui que os generics entram em cena — um poderoso mecanismo de tipagem em Kotlin que permite escrever código flexível sem sacrificar a segurança de tipos. Neste artigo, vamos explorar generics em Kotlin do simples ao complexo: classes básicas, funções, variância (in/out), tipos reificados e star projections. Você aprenderá como tornar o código mais limpo e o compilador seu maior aliado.
\n\nEste artigo foi preparado para o portal educacional pythonlib.ru — seu guia para o mundo das linguagens de programação modernas.
\n\n1. Fundamentos: o que são generics em Kotlin?
\n\nGenerics (tipos genéricos) permitem parametrizar classes, interfaces e funções com um tipo que será especificado no momento do uso. Isso elimina a duplicação de código e fornece controle sobre os tipos em tempo de compilação.
\n\nVeja um exemplo simples:
\n\nclass Box<T>(val item: T)\n\nfun main() {\n val bookBox = Box<String>("Kotlin in Action")\n val intBox = Box<Int>(42)\n // O compilador sabe o tipo!\n val title: String = bookBox.item\n}\n\nPontos-chave:
\n- \n
<T>— parâmetro de tipo (pode ser qualquer letra, mas por convenção T, R, E, K, V). \n - O tipo é fixado ao criar o objeto — sem casts em tempo de execução. \n
- Generics funcionam apenas em tempo de compilação (type erasure), mas Kotlin adiciona "reificação" (mais sobre isso adiante). \n
Funções genéricas
\nA tipagem em Kotlin também funciona para funções. Por exemplo, uma função que retorna o primeiro elemento de uma lista:
\n\nfun <T> firstItem(list: List<T>): T? = list.firstOrNull()\n\nfun main() {\n val numbers = listOf(1, 2, 3)\n val first: Int? = firstItem(numbers) // T = Int\n println(first) // 1\n}\n\nObserve: Kotlin consegue inferir o tipo a partir do contexto — não é necessário escrever firstItem<Int>(numbers).
2. Variância: in e out — como não quebrar a segurança de tipos
\n\nO tópico mais difícil para iniciantes é a variância. Ela responde à pergunta: pode List<String> ser um subtipo de List<Any>?
Em Java, isso é resolvido com wildcards (? extends / ? super). Em Kotlin, com out e in.
out (covariância) — apenas leitura
\nA marcação out significa que o tipo genérico só pode ser "fornecido" (retornado de métodos). Exemplo — List<out T>.
interface Source<out T> {\n fun next(): T // OK: T na saída\n // fun add(item: T) // Erro! T na entrada é proibido\n}\n\nfun demo(source: Source<String>) {\n val anySource: Source<Any> = source // out permite isso\n}\n\nPor que isso é seguro? Se só podemos ler String da fonte, então ela pode ser considerada uma fonte de Any.
in (contravariância) — apenas escrita
\nin — ao contrário: o tipo só pode estar na entrada. Clássico — Comparable.
interface Comparable<in T> {\n operator fun compareTo(other: T): Int\n}\n\nfun demo(comp: Comparable<Number>) {\n val intComp: Comparable<Int> = comp // in permite isso\n}\n\nSe Comparable<Number> consegue comparar qualquer número, então certamente conseguirá comparar Int.
in out (invariância) — padrão
\nSe nem in nem out forem especificados, o tipo é inv