Pular para o conteúdo

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

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

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.

Com Any, o cast as Int compila e a ClassCastException só aparece na execução; com Caixa<T>, a chamada com texto é recusada antes de rodar.
figura 1 · Any com cast ou T com checagem antes de rodar

À 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.

O compilador deduz T do argumento List<String> e dá ao retorno o tipo String; sem argumento nem tipo declarado, como em emptyList(), T fica sem pista.
figura 2 · Como o compilador descobre T numa chamada

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. Int e String servem. Uma classe Pedido que não implementa Comparable é recusada pelo compilador, e a mensagem diz que Pedido não satisfaz Comparable. Entre os sinais de menor e maior cabe um só limite. Com mais de um, uso a , por exemplo exigindo Comparable e Any. O ganho é que o erro aparece na compilação e não quando o código já está rodando.

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.

buscar devolve Pedido?: se a chave existe, o valor é um Pedido; se não existe, é null, e declarar Pedido direto na variável não compila sem o tratamento.
figura 3 · buscar devolve Pedido? e o uso decide o que fazer
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

  1. Escreva fun <T> repetir(item: T, vezes: Int): List<T>. Antes de rodar, diga o tipo de cincos em val cincos = repetir(5, 2), e confira com sum().
  2. Escreva fun <T : Comparable<T>> menorDe(lista: List<T>): T?, que devolve o menor item ou null se 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 <>, como T, que representa um tipo ainda não escolhido.
  • Argumento de tipo: tipo escolhido na chamada ou na declaração, como String em Caixa<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 aceita null, como T?.
  • 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 ClassCastException vem do JDK. Ambos mudam com a versão, e foram iguais nos dois JDKs testados.
  • T : Any e o limite padrão Any? 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.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?