Trilha de Kotlin · parte 14 de 59
Genéricos básicos: classes e funções para vários tipos
Como escrever classes e funções genéricas em Kotlin com T, deixar o compilador deduzir o tipo, limitar T com Comparable e lidar com null, com saídas reais.
intermediárioPublicada em · 13 min de leitura
Genéricos básicos: classes e funções para vários tipos
Pense numa caixa de correio com etiqueta. Se a etiqueta diz “cartas”, só entram cartas. Se não diz nada, qualquer objeto cabe, e a surpresa aparece quando alguém abre a caixa esperando outra coisa. Um genérico é uma caixa cuja etiqueta o código preenche a cada uso, e o compilador confere a etiqueta antes de a encomenda sair. A imagem tem limite: a etiqueta não fica colada para sempre, ela é escrita de novo em cada uso.
Esta aula supõe que você já declara classes (Classes, objetos e propriedades) e funções (Funções). Você já usou genéricos sem ver a sintaxe: List<String> é uma lista com a etiqueta preenchida (Coleções e lambdas). Aqui você escreve os seus, o que prepara o terreno para Sequence<T> e Result<T> em Coleções imutáveis e Sequence e Tratamento funcional de erros. Os trechos rodaram com Kotlin 2.4.21 e Gradle 9.7.0, no JDK 25 e no JDK 21, com saídas e posições de erro iguais. Os blocos não trazem package, então as posições batem ao colar o trecho em Main.kt.
o mapa da aula
A seção 1 mostra o problema que genéricos resolvem. As seções 2 a 5 cobrem classe genérica, função genérica, e variáveis de tipo, com um repositório em memória. As seções 6 e 7 tratam de restrições e de tipos anuláveis, a 8 explica List e MutableList, e a 9 reúne os erros comuns.
1. o problema: serve para tudo e quebra depois
Veja a solução ingênua: uma caixa declarada com Any, o tipo de que todos os outros são subtipos. Ela funciona enquanto você só guarda e imprime. Quando precisa do conteúdo como número, entra o cast com as, e o compilador aceita a promessa sem conferir nada.
class Caixa(val conteudo: Any)
fun main() {
val caixa = Caixa("Stronger")
println("guardado: ${caixa.conteudo}")
try {
val numero = caixa.conteudo as Int
println("número: $numero")
} catch (e: ClassCastException) {
println("erro: ${e.message}")
}
}
guardado: Stronger
erro: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
O código compila, e a falha só acontece quando a linha com as Int roda. O as faz o papel do funcionário que assina o recebimento sem abrir o pacote. Você trocou a checagem da compilação por um erro na execução.
2. a classe genérica: Caixa
Um fica entre <>, depois do nome da classe. T é só um nome, por convenção uma letra maiúscula. Ao escrever Caixa<String>, todo T da classe passa a ser String.
class Caixa<T>(val conteudo: T)
fun main() {
val nome = Caixa("Stronger")
val numero = Caixa(10)
println("${nome.conteudo.length} letras")
println(numero.conteudo + 5)
}
8 letras
15
O compilador sabe que nome.conteudo é String, então .length compila. Agora tente pôr texto numa caixa declarada como de Int:
class Caixa<T>(val conteudo: T)
fun main() {
val numero: Caixa<Int> = Caixa("texto")
println(numero.conteudo)
}
e: .../src/main/kotlin/Main.kt:4:28 Initializer type mismatch: expected 'Caixa<Int>', actual 'Caixa<String>'.
A linha que a seção 1 deixou passar é recusada antes de rodar. Ninguém escreveu essa checagem à mão: ela sai do tipo. A figura compara os dois casos: o cast que só falha na execução e a chamada que o compilador recusa.
À esquerda, a falha chega na execução; à direita, o compilador recusa a chamada antes de rodar.
3. função genérica e inferência
Funções também recebem <T>, antes do nome. Em primeiro, o T aparece no parâmetro List<T> e no retorno.
fun <T> primeiro(lista: List<T>): T = lista.first()
fun main() {
val nome = primeiro(listOf("maçã", "banana"))
println("${nome.length} letras")
println(primeiro(listOf(1, 2, 3)) + 10)
println(primeiro<Int>(listOf(7)))
}
4 letras
11
7
Na primeira chamada não há primeiro<String>. O compilador viu uma List<String>, concluiu que T é String e deu esse tipo ao resultado. Essa dedução se chama inferência do . Quando o argumento não traz pista, ela não tem de onde sair:
fun main() {
val vazia = emptyList()
println(vazia.size)
}
e: .../src/main/kotlin/Main.kt:2:17 Cannot infer type for type parameter 'T'. Specify it explicitly.
Basta declarar o tipo na variável: val vazia: List<String> = emptyList() compila e imprime 0. A figura percorre os dois caminhos. O de primeiro termina no retorno String. O de emptyList() chega direto ao quadro sem pista, e a seta bloqueada no losango mostra o ramo do não, que é o que o compilador recusa.
Pense no garçom que anota o pedido na comanda. Se a comanda diz o prato, ele não pergunta. O limite da analogia está aí: o garçom ainda pode perguntar, enquanto o compilador só deduz com o que está escrito na chamada ou na declaração.
4. o repositório: uma classe genérica de verdade
Caixas ilustram a sintaxe, mas um uso real pede uma classe que guarde vários itens e os encontre por chave. O repositório abaixo atravessa a aula. Ele recebe, na construção, uma função que diz a chave de cada item, e as lambdas dessa posição são as de Coleções e lambdas.
class Repositorio<T>(private val chaveDe: (T) -> Int) {
private val itens = mutableListOf<T>()
fun salvar(item: T) {
itens.add(item)
}
fun buscar(chave: Int): T? = itens.firstOrNull { chaveDe(it) == chave }
fun quantidade(): Int = itens.size
}
class Pedido(val id: Int, val valor: Int)
fun main() {
val pedidos = Repositorio<Pedido> { it.id }
pedidos.salvar(Pedido(1, 120))
pedidos.salvar(Pedido(2, 45))
println("quantidade: ${pedidos.quantidade()}")
println("pedido 2: ${pedidos.buscar(2)?.valor}")
println("pedido 9: ${pedidos.buscar(9)?.valor}")
}
quantidade: 2
pedido 2: 45
pedido 9: null
Em Repositorio<Pedido> { it.id }, T vira Pedido, e o compilador garante que só entram pedidos. O retorno de buscar é T?, porque pode não haver item com aquela chave. A seção 9 mostra o que isso exige de quem chama.
5. várias variáveis de tipo
Uma classe pode ter mais de um parâmetro. Par<A, B> guarda um valor de cada tipo, e cada posição é conferida separadamente.
class Par<A, B>(val esquerda: A, val direita: B)
fun main() {
val pedido = Par("Ana", 120)
println("${pedido.esquerda} pagou ${pedido.direita}")
println(pedido.direita + 5)
}
Ana pagou 120
125
Com Par("Ana", 120), o compilador infere Par<String, Int>, e pedido.direita + 5 compila porque a segunda posição é Int. Troque o 120 por "120" e a mesma soma continua compilando, mas o resultado muda: String + Int concatena e imprime 1205. Quem é recusado é o operador -. Com Par("Ana", "120"), a subtração falha, porque o compilador não encontra operador - para um String:
class Par<A, B>(val esquerda: A, val direita: B)
fun main() {
val pedido = Par("Ana", "120")
println("${pedido.esquerda} pagou ${pedido.direita}")
println(pedido.direita - 5)
}
e: .../src/main/kotlin/Main.kt:6:28 Unresolved reference 'minus' for operator '-' on receiver of type 'String'.
A mensagem diz que não existe minus com String do lado esquerdo, e a coluna 28 aponta para o próprio operador. Cada posição do par leva o tipo que recebeu, e as operações permitidas dependem desse tipo.
6. restrições: T : Comparable e where
Às vezes T precisa de uma capacidade. A função maiorDe compara com >=, e isso só existe para tipos comparáveis. A restrição T : Comparable<T> é um (upper bound).
fun <T : Comparable<T>> maiorDe(a: T, b: T): T = if (a >= b) a else b
fun main() {
println(maiorDe(3, 7))
println(maiorDe("maçã", "banana"))
}
7
maçã
Int e String servem, porque implementam Comparable. Um tipo que não implementa é recusado:
class Pedido(val id: Int, val valor: Int)
fun <T : Comparable<T>> maiorDe(a: T, b: T): T = if (a >= b) a else b
fun main() {
val maior = maiorDe<Pedido>(Pedido(1, 120), Pedido(2, 45))
println(maior.valor)
}
e: .../src/main/kotlin/Main.kt:6:17 Inapplicable candidate(s): fun <T : Comparable<T>> maiorDe(a: T, b: T): T
e: .../src/main/kotlin/Main.kt:6:25 Type argument is not within its bounds: type parameter 'T (of fun <T : Comparable<T>> maiorDe)' must be subtype of 'Comparable<Pedido>', but actual: 'Pedido'.
A segunda linha da mensagem diz o principal: Pedido não satisfaz Comparable<Pedido>. Se a classe precisa ser comparável, ela tem de implementar Comparable. Entre <> cabe um só limite. Com mais de um, a lista vai para depois da assinatura, com where. A função abaixo usa as duas restrições, Comparable<T> e Any, e o max() da biblioteca padrão devolve o maior item:
fun <T> maiorDeLista(lista: List<T>): T where T : Comparable<T>, T : Any = lista.max()
fun main() {
println(maiorDeLista(listOf(3, 9, 4)))
}
9
7. tipos anuláveis: T?, o limite padrão e T : Any
Sem restrição, o limite padrão de T é Any?: qualquer tipo, anulável ou não. Por isso Caixa<String?>(null) e descreve(null) compilam.
class Caixa<T>(val conteudo: T)
fun <T> descreve(valor: T): String = "valor: $valor"
fun main() {
val vazia = Caixa<String?>(null)
println(vazia.conteudo ?: "sem texto")
println(descreve(null))
println(descreve(42))
}
sem texto
valor: null
valor: 42
Para recusar null, use T : Any:
fun <T : Any> presente(valor: T): T = valor
fun main() {
println(presente(null))
}
e: .../src/main/kotlin/Main.kt:4:13 Cannot infer type for type parameter 'T'. Specify it explicitly.
e: .../src/main/kotlin/Main.kt:4:22 Null cannot be a value of a non-null type 'uninferred T (of fun <T : Any> presente)'.
A segunda linha diz que null não pode ser valor de um T não nulo. Pense numa gaveta de achados e perdidos: você pede o guarda-chuva azul e ouve “não temos”. O T? de buscar é essa resposta dupla. A analogia para no ponto principal: na gaveta, a pessoa avisa; no código, quem recebe o null precisa tratar o caso.
Pergunta de entrevistaPara que servem as restrições de tipo em genéricos Kotlin, como T : Comparable?
Sem restrição, T pode ser qualquer tipo, então só posso usar o que todos os tipos têm. Quando a função precisa de uma capacidade, declaro um limite superior. Na aula, maiorDe compara com >=, o que só existe para tipos comparáveis, então uso T : Comparable
8. listas somente leitura e mutáveis: a regra que parece contraditória
Se String é um Any, uma List<String> serve como List<Any>? Para a lista somente leitura, sim:
fun main() {
val nomes: MutableList<String> = mutableListOf("Ana")
val leitura: List<Any> = nomes
println(leitura)
}
[Ana]
Com MutableList, não:
fun main() {
val nomes: MutableList<String> = mutableListOf("Ana")
val qualquer: MutableList<Any> = nomes
println(qualquer)
}
e: .../src/main/kotlin/Main.kt:3:36 Initializer type mismatch: expected 'MutableList<Any>', actual 'MutableList<String>'.
Pela referência MutableList<Any>, a lista poderia receber um item que não é String. A List só devolve itens, então esse risco não existe nela. A regra é de variância, explicada em Variância e genéricos reified, onde aparece também List<*>, a .
Pergunta de entrevistaUm parâmetro de tipo T em Kotlin aceita null? Como você impede isso?
Sem restrição, o limite superior padrão de T é Any?, ou seja, qualquer tipo, anulável ou não. Por isso Caixa<String?>(null) compila. Se quero recusar null, declaro T : Any, e o compilador rejeita um null como valor de um T não nulo. Essa distinção importa em APIs genéricas como um repositório: uma função buscar que devolve T? sinaliza que o item pode não existir, e quem recebe o null precisa tratar o caso. Uso T : Any quando null não faz sentido para o tipo e deixo o padrão quando a ausência de valor é legítima. Assim a decisão fica explícita na assinatura da API.
9. erros comuns e boas práticas
tratar T? como T
O buscar devolve Pedido?, e declarar a variável como Pedido não compila; a figura mostra o caminho bloqueado.
class Repositorio<T>(private val chaveDe: (T) -> Int) {
private val itens = mutableListOf<T>()
fun salvar(item: T) {
itens.add(item)
}
fun buscar(chave: Int): T? = itens.firstOrNull { chaveDe(it) == chave }
}
class Pedido(val id: Int, val valor: Int)
fun main() {
val pedidos = Repositorio<Pedido> { it.id }
pedidos.salvar(Pedido(2, 45))
val pedido: Pedido = pedidos.buscar(2)
println(pedido.valor)
}
e: .../src/main/kotlin/Main.kt:16:24 Initializer type mismatch: expected 'Pedido', actual 'Pedido?'.
Todo mundo faz isso pelo menos uma vez, e quem diz o contrário ainda não escreveu repositório. Trate o caso com ?., como na seção 4, ou com ?:, como em Null safety e tipos nuláveis.
usar Any para ganhar flexibilidade
É a armadilha da seção 1. Antes de escrever Any numa assinatura, veja se um T resolve. Se resolver, o compilador passa a conferir o que antes só o cast fazia.
boas práticas
Restrinja T só quando o corpo usa a capacidade, porque cada restrição vira um contrato para quem chama. Dê nomes úteis quando houver mais de um parâmetro, como Chave e Valor, porque o nome aparece nas mensagens do compilador.
10. exercício guiado: repositório de clientes
Copie a classe Repositorio da seção 4 para um arquivo novo. Crie class Cliente(val id: Int, val nome: String) e um Repositorio<Cliente> { it.id }. Salve Cliente(7, "Ana"), busque 7 e 8 com ?.nome e ?:, e confira a saída abaixo.
class Repositorio<T>(private val chaveDe: (T) -> Int) {
private val itens = mutableListOf<T>()
fun salvar(item: T) {
itens.add(item)
}
fun buscar(chave: Int): T? = itens.firstOrNull { chaveDe(it) == chave }
fun quantidade(): Int = itens.size
}
class Cliente(val id: Int, val nome: String)
fun main() {
val clientes = Repositorio<Cliente> { it.id }
clientes.salvar(Cliente(7, "Ana"))
println(clientes.buscar(7)?.nome)
println(clientes.buscar(8)?.nome ?: "não encontrado")
println("quantidade: ${clientes.quantidade()}")
}
Ana
não encontrado
quantidade: 1
11. exercícios com resposta
- Escreva
fun <T> repetir(item: T, vezes: Int): List<T>. Antes de rodar, diga o tipo decincosemval cincos = repetir(5, 2), e confira comsum(). - Escreva
fun <T : Comparable<T>> menorDe(lista: List<T>): T?, que devolve o menor item ounullse a lista estiver vazia. Teste com números, textos e uma lista vazia tipada.
Resposta do primeiro:
fun <T> repetir(item: T, vezes: Int): List<T> = List(vezes) { item }
fun main() {
println(repetir("ok", 3))
val cincos = repetir(5, 2)
println(cincos.sum())
}
[ok, ok, ok]
10
Resposta do segundo:
fun <T : Comparable<T>> menorDe(lista: List<T>): T? {
if (lista.isEmpty()) return null
var menor = lista[0]
for (item in lista) {
if (item < menor) menor = item
}
return menor
}
fun main() {
println(menorDe(listOf(4, 2, 9)))
println(menorDe(listOf("pão", "arroz")))
val vazio: List<Int> = emptyList()
println(menorDe(vazio))
}
2
arroz
null
12. glossário
- Parâmetro de tipo: nome entre
<>, comoT, que representa um tipo ainda não escolhido. - Argumento de tipo: tipo escolhido na chamada ou na declaração, como
StringemCaixa<String>. - Inferência: dedução, pelo compilador, do argumento de tipo a partir dos argumentos ou do tipo declarado.
- Limite superior (upper bound): restrição
T : Tipo, que aceita apenas subtipos do tipo indicado. - Cláusula where: lista de restrições depois da assinatura.
- Tipo anulável: tipo com
?, que aceitanull, comoT?. - Limite padrão
Any?: limite de um parâmetro sem restrição explícita. - Projeção com asterisco: forma
List<*>de aceitar uma lista de qualquer tipo. Assunto de Variância e genéricos reified.
13. o que não foi confirmado ou medido
- Testei no Mac com JDK 25 (bytecode versão 69) e JDK 21 (versão 65), Kotlin 2.4.21 e Gradle 9.7.0. Outros sistemas e outros JDKs não foram testados.
- As mensagens de erro são do compilador 2.4.21, e o texto da
ClassCastExceptionvem do JDK. Ambos mudam com a versão, e foram iguais nos dois JDKs testados. T : Anye o limite padrãoAny?foram conferidos pelos exemplos e pela documentação, não por inspeção de bytecode. Os cliques do IntelliJ não foram testados.
Fontes
Consultada em 04/11/2026. A página traz a data de 29/09/2026 no rodapé e não informa versão do Kotlin.
- Kotlin, genéricos (parâmetros de tipo, limites superiores, cláusula where, limite padrão
Any?, inferência): https://kotlinlang.org/docs/generics.html