Introdução a Generics em Kotlin
Generics (generalizações) — um dos recursos-chave da linguagem Kotlin, que permite escrever código flexível, type-safe e reutilizável. Se você já escreveu List<String> ou Map<Int, User>, você já usou genéricos, mesmo sem pensar nisso. Mas o que está por trás desses colchetes angulares e como extrair o máximo deles? Neste artigo, vamos analisar para que servem os Generics em Kotlin, como aplicá-los na prática e quais recursos avançados (variance, reified, where) ajudarão você a escrever um código ainda mais limpo e seguro.
Este artigo é destinado a desenvolvedores que já conhecem os fundamentos do Kotlin e desejam aprofundar seus conhecimentos. Vamos abordar tudo: desde a sintaxe básica até exemplos reais de código em produção.
Para que servem os Generics? O problema sem eles
Imagine que você precisa escrever uma função que recebe uma lista de quaisquer elementos e retorna o primeiro elemento. Sem genéricos, você escreveria assim:
fun getFirst(list: List<Any>): Any { return list[0]}Agora, se você passar List<String>, receberá Any como saída. Para trabalhar com isso como uma string, será necessário fazer uma conversão explícita de tipos:
val first = getFirst(listOf("Kotlin", "Java")) as Stringfirst.length // Perigoso! Pode lançar ClassCastExceptionIsso é inconveniente e inseguro. O erro só aparecerá em tempo de execução. Generics resolvem esse problema, preservando a informação do tipo em tempo de compilação.
Principais vantagens dos Generics:
- Type safety — o compilador verifica os tipos em tempo de compilação, não em tempo de execução.
- Eliminação de conversões explícitas — o código fica mais limpo e legível.
- Reutilização — uma classe ou função funciona com diferentes tipos.
Sintaxe básica: classes, funções e restrições
Classes genéricas
A maneira mais simples de começar é declarar uma classe com um ou mais parâmetros de tipo. Os parâmetros de tipo são indicados entre colchetes angulares <T> (a letra pode ser qualquer uma, mas tradicionalmente usam T, E, K, V):
class Box<T>(val value: T)
fun main() { val stringBox = Box(\"Kotlin\") // O tipo é inferido automaticamente val intBox = Box(42) // Box<Int>
println(stringBox.value.length) // Funciona sem conversão! println(intBox.value + 1) // 43}Kotlin consegue inferir o tipo, portanto não é necessário escrever Box<String> — basta Box(\"Kotlin\").
Funções genéricas
Funções também podem ser genéricas. O parâmetro de tipo é indicado antes do nome da função:
fun <T> getMiddleElement(list: List<T>): T? { if (list.isEmpty()) return null return list[list.size / 2]}
fun main() { val numbers = listOf(1, 2, 3, 4, 5) val middle = getMiddleElement(numbers) // O tipo Int? é inferido automaticamente println(middle) // 3}Restrições (where / upper bound)
Às vezes é necessário limitar quais tipos podem ser usados em um genérico. Por exemplo, para chamar o método .compareTo(), o tipo deve implementar a interface Comparable:
fun <T : Comparable<T>> maxOfTwo(a: T, b: T): T { return if (a >= b) a else b}
fun main() { println(maxOfTwo(10, 20)) // 20 println(maxOfTwo(\"A\", \"B\")) // B // maxOfTwo(10, \"A\") // Erro de compilação: tipos não coincidem}Se forem necessárias várias restrições, use a palavra-chave where:
interface Printable { fun print()}
fun <T> process(item: T) where T : Comparable<T>, T : Printable