Este codelab faz parte do curso de treinamento do Kotlin para programadores. Você vai aproveitar mais este curso se fizer os codelabs em sequência. Dependendo do seu conhecimento, talvez você possa passar mais rápido por algumas seções. Este curso é destinado a programadores que conhecem uma linguagem orientada a objetos e querem aprender Kotlin.
Introdução
Neste codelab, você vai conhecer classes, funções e métodos genéricos e como eles funcionam no Kotlin.
Em vez de criar um único app de exemplo, as lições deste curso foram criadas para aumentar seu conhecimento, mas são semi-independentes umas das outras para que você possa ler as seções com que já está familiarizado. Para unificar os exemplos, muitos deles usam um tema de aquário. Se quiser conferir a história completa do aquário, acesse o curso da Udacity Treinamento do Kotlin para programadores (link em inglês).
O que você já precisa saber
- A sintaxe de funções, classes e métodos do Kotlin
- Como criar uma nova classe no IntelliJ IDEA e executar um programa
O que você vai aprender
- Como trabalhar com classes, métodos e funções genéricos
Atividades deste laboratório
- Criar uma classe genérica e adicionar restrições
- Criar tipos
ineout - Criar funções, métodos e funções de extensão genéricos
Introdução aos tipos genéricos
O Kotlin, assim como muitas linguagens de programação, tem tipos genéricos. Um tipo genérico permite tornar uma classe genérica e, assim, muito mais flexível.
Imagine que você esteja implementando uma classe MyList que contém uma lista de itens. Sem os tipos genéricos, você precisaria implementar uma nova versão de MyList para cada tipo: uma para Double, uma para String e uma para Fish. Com os tipos genéricos, você pode tornar a lista genérica para que ela possa conter qualquer tipo de objeto. É como tornar o tipo um caractere curinga que se encaixa em muitos tipos.
Para definir um tipo genérico, coloque T entre colchetes angulares <T> após o nome da classe. Você pode usar outra letra ou um nome mais longo, mas a convenção para um tipo genérico é T.
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}Você pode referenciar T como se fosse um tipo normal. O tipo de retorno para get() é T, e o parâmetro para addItem() é do tipo T. É claro que as listas genéricas são muito úteis, então a classe List é integrada ao Kotlin.
Etapa 1: criar uma hierarquia de tipos
Nesta etapa, você vai criar algumas classes para usar na próxima etapa. A criação de subclasses foi abordada em um codelab anterior, mas aqui está uma breve revisão.
- Para manter o exemplo organizado, crie um novo pacote em src e chame-o de
generics. - No pacote generics, crie um arquivo
Aquarium.kt. Isso permite redefinir coisas usando os mesmos nomes sem conflitos. Portanto, o restante do código deste codelab vai para esse arquivo. - Crie uma hierarquia de tipos de abastecimento de água. Comece transformando
WaterSupplyem uma classeopenpara que ela possa ser transformada em subclasse. - Adicione um parâmetro booleano
var,needsProcessing. Isso cria automaticamente uma propriedade mutável, além de um getter e um setter. - Crie uma subclasse
TapWaterque estendaWaterSupplye transmitatrueparaneedsProcessing, porque a água da torneira contém aditivos que são ruins para os peixes. - Em
TapWater, defina uma função chamadaaddChemicalCleaners()que defineneedsProcessingcomofalsedepois de limpar a água. A propriedadeneedsProcessingpode ser definida emTapWaterporque épublicpor padrão e acessível a subclasses. Este é o código concluído.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}- Crie mais duas subclasses de
WaterSupply, chamadasFishStoreWatereLakeWater.FishStoreWaternão precisa de processamento, masLakeWaterprecisa ser filtrado com o métodofilter(). Depois da filtragem, não é necessário processar novamente. Portanto, emfilter(), definaneedsProcessing = false.
class FishStoreWater : WaterSupply(false)
class LakeWater : WaterSupply(true) {
fun filter() {
needsProcessing = false
}
}Se você precisar de mais informações, consulte a lição anterior sobre herança em Kotlin.
Etapa 2: criar uma classe genérica
Nesta etapa, você vai modificar a classe Aquarium para oferecer suporte a diferentes tipos de abastecimento de água.
- Em Aquarium.kt, defina uma classe
Aquariumcom<T>entre colchetes após o nome da classe. - Adicione uma propriedade imutável
waterSupplydo tipoTaAquarium.
class Aquarium<T>(val waterSupply: T)- Escreva uma função chamada
genericsExample(). Como não faz parte de uma classe, ele pode ficar no nível superior do arquivo, como a funçãomain()ou as definições de classe. Na função, crie umAquariume transmita umWaterSupply. Como o parâmetrowaterSupplyé genérico, especifique o tipo entre colchetes angulares<>.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}- Em
genericsExample(), seu código pode acessar owaterSupplydo aquário. Como é do tipoTapWater, você pode chamaraddChemicalCleaners()sem conversões de tipo.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- Ao criar o objeto
Aquarium, você pode remover os colchetes angulares e o que está entre eles porque o Kotlin tem inferência de tipo. Portanto, não há motivo para dizerTapWaterduas vezes ao criar a instância. O tipo pode ser inferido pelo argumento deAquarium, mas ainda vai criar umAquariumdo tipoTapWater.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}- Para conferir o que está acontecendo, imprima
needsProcessingantes e depois de chamaraddChemicalCleaners(). Confira abaixo a função concluída.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
aquarium.waterSupply.addChemicalCleaners()
println("water needs processing: ${aquarium.waterSupply.needsProcessing}")
}- Adicione uma função
main()para chamargenericsExample(). Em seguida, execute o programa e observe o resultado.
fun main() {
genericsExample()
}⇒ water needs processing: true water needs processing: false
Etapa 3: especifique mais
Genérico significa que você pode transmitir quase qualquer coisa, e às vezes isso é um problema. Nesta etapa, você vai tornar a classe Aquarium mais específica sobre o que pode ser colocado nela.
- Em
genericsExample(), crie umAquarium, transmitindo uma string para owaterSupplye imprima a propriedadewaterSupplydo aquário.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}- Execute o programa e observe o resultado.
⇒ string
O resultado é a string transmitida, porque Aquarium não impõe limitações a T.. Qualquer tipo, incluindo String, pode ser transmitido.
- Em
genericsExample(), crie outraAquarium, transmitindonullpara owaterSupply. SewaterSupplyfor nulo, imprima"waterSupply is null".
fun genericsExample() {
val aquarium3 = Aquarium(null)
if (aquarium3.waterSupply == null) {
println("waterSupply is null")
}
}- Execute o programa e observe o resultado.
⇒ waterSupply is null
Por que é possível transmitir null ao criar um Aquarium? Isso é possível porque, por padrão, T representa o tipo anulável Any?, que está no topo da hierarquia de tipos. O seguinte é equivalente ao que você digitou antes.
class Aquarium<T: Any?>(val waterSupply: T)- Para não permitir a transmissão de
null, faça com queTseja do tipoAnyexplicitamente, removendo o?apósAny.
class Aquarium<T: Any>(val waterSupply: T)Nesse contexto, Any é chamada de restrição genérica. Isso significa que qualquer tipo pode ser transmitido para T, desde que não seja null.
- O que você realmente quer é garantir que apenas um
WaterSupply(ou uma das subclasses dele) possa ser transmitido paraT. SubstituaAnyporWaterSupplypara definir uma restrição genérica mais específica.
class Aquarium<T: WaterSupply>(val waterSupply: T)Etapa 4: adicionar mais verificações
Nesta etapa, você vai aprender sobre a função check() para garantir que seu código esteja funcionando como esperado. A função check() é uma função de biblioteca padrão em Kotlin. Ele funciona como uma declaração e vai gerar um IllegalStateException se o argumento for avaliado como false.
- Adicione um método
addWater()à classeAquariumpara adicionar água, com umcheck()que garante que você não precise processar a água primeiro.
class Aquarium<T: WaterSupply>(val waterSupply: T) {
fun addWater() {
check(!waterSupply.needsProcessing) { "water supply needs processing first" }
println("adding water from $waterSupply")
}
}Nesse caso, se needsProcessing for verdadeiro, check() vai gerar uma exceção.
- Em
genericsExample(), adicione um código para criar umAquariumcomLakeWatere adicione água a ele.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}- Execute o programa. Você vai receber uma exceção porque a água precisa ser filtrada primeiro.
⇒ Exception in thread "main" java.lang.IllegalStateException: water supply needs processing first
at Aquarium.generics.Aquarium.addWater(Aquarium.kt:21)- Adicione uma chamada para filtrar a água antes de adicioná-la ao
Aquarium. Agora, quando você executa o programa, nenhuma exceção é gerada.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.waterSupply.filter()
aquarium4.addWater()
}⇒ adding water from generics.LakeWater@880ec60
O texto acima aborda os conceitos básicos de tipos genéricos. As tarefas a seguir abordam mais detalhes, mas o conceito importante é como declarar e usar uma classe genérica com uma restrição genérica.
Nesta tarefa, você vai aprender sobre tipos de entrada e saída com classes genéricas. Um tipo in é um tipo que só pode ser transmitido para uma classe, não retornado. Um tipo out é um tipo que só pode ser retornado de uma classe.
Confira a classe Aquarium e você vai perceber que o tipo genérico só é retornado ao receber a propriedade waterSupply. Não há métodos que usam um valor do tipo T como parâmetro, exceto para defini-lo no construtor. O Kotlin permite definir tipos out exatamente para esse caso e pode inferir informações extras sobre onde os tipos são seguros para uso. Da mesma forma, é possível definir tipos in para tipos genéricos que são transmitidos apenas para métodos, não retornados. Isso permite que o Kotlin faça verificações extras para a segurança do código.
Os tipos in e out são diretivas para o sistema de tipos do Kotlin. Explicar todo o sistema de tipos está fora do escopo deste bootcamp (é bem complexo). No entanto, o compilador vai sinalizar os tipos que não estão marcados como in e out adequadamente, então você precisa saber sobre eles.
Etapa 1: definir um tipo de saída
- Na classe
Aquarium, mudeT: WaterSupplypara ser do tipoout.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}- No mesmo arquivo, fora da classe, declare uma função
addItemTo()que espera umAquariumdeWaterSupply.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")- Chame
addItemTo()degenericsExample()e execute o programa.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}⇒ item added
O Kotlin pode garantir que addItemTo() não fará nada que não seja seguro para o tipo com o WaterSupply genérico, porque ele é declarado como um tipo out.
- Se você remover a palavra-chave
out, o compilador vai gerar um erro ao chamaraddItemTo(), porque o Kotlin não pode garantir que você não está fazendo nada inseguro com o tipo.
Etapa 2: definir um tipo "in"
O tipo in é semelhante ao tipo out, mas para tipos genéricos que são transmitidos apenas para funções, não retornados. Se você tentar retornar um tipo in, vai receber um erro do compilador. Neste exemplo, você vai definir um tipo in como parte de uma interface.
- Em Aquarium.kt, defina uma interface
Cleanerque usa umTgenérico restrito aWaterSupply. Como ele é usado apenas como um argumento paraclean(), você pode transformá-lo em um parâmetroin.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}- Para usar a interface
Cleaner, crie uma classeTapWaterCleanerque implementeCleanerpara limparTapWateradicionando produtos químicos.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}- Na classe
Aquarium, atualizeaddWater()para receber umCleanerdo tipoTe limpe a água antes de adicioná-la.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
fun addWater(cleaner: Cleaner<T>) {
if (waterSupply.needsProcessing) {
cleaner.clean(waterSupply)
}
println("water added")
}
}- Atualize o exemplo de código
genericsExample()para criar umTapWaterCleaner, umAquariumcomTapWatere adicione água usando o limpador. Ele vai usar o limpador conforme necessário.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}O Kotlin usa as informações de tipo in e out para garantir que seu código use os genéricos com segurança. Out e in são fáceis de lembrar: os tipos out podem ser transmitidos como valores de retorno, e os tipos in podem ser transmitidos como argumentos.

Se quiser saber mais sobre os tipos de problemas que os tipos de entrada e saída resolvem, consulte a documentação, que aborda esses assuntos em detalhes.
Nesta tarefa, você vai aprender sobre funções genéricas e quando usá-las. Normalmente, é uma boa ideia criar uma função genérica sempre que ela recebe um argumento de uma classe que tem um tipo genérico.
Etapa 1: criar uma função genérica
- Em generics/Aquarium.kt, crie uma função
isWaterClean()que use umAquarium. É necessário especificar o tipo genérico do parâmetro. Uma opção é usarWaterSupply.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}Mas isso significa que Aquarium precisa ter um parâmetro de tipo out para ser chamado. Às vezes, out ou in são muito restritivos porque você precisa usar um tipo para entrada e saída. É possível remover o requisito out tornando a função genérica.
- Para tornar a função genérica, coloque colchetes angulares após a palavra-chave
funcom um tipo genéricoTe quaisquer restrições, neste caso,WaterSupply. MudeAquariumpara ser restrito porTem vez deWaterSupply.
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) {
println("aquarium water is clean: ${!aquarium.waterSupply.needsProcessing}")
}T é um parâmetro de tipo para isWaterClean() que está sendo usado para especificar o tipo genérico do aquário. Esse padrão é muito comum, e é uma boa ideia dedicar um tempo para trabalhar nele.
- Chame a função
isWaterClean()especificando o tipo entre colchetes angulares logo após o nome da função e antes dos parênteses.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean<TapWater>(aquarium)
}- Devido à inferência de tipo do argumento
aquarium, o tipo não é necessário. Portanto, remova-o. Execute o programa e observe a saída.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}⇒ aquarium water is clean: false
Etapa 2: criar um método genérico com um tipo concretizado
Você também pode usar funções genéricas para métodos, mesmo em classes que têm seu próprio tipo genérico. Nesta etapa, você vai adicionar um método genérico a Aquarium que verifica se ele tem um tipo de WaterSupply.
- Na classe
Aquarium, declare um método,hasWaterSupplyOfType(), que usa um parâmetro genéricoR(Tjá está em uso) restrito aWaterSupplye retornatruesewaterSupplyfor do tipoR. É como a função que você declarou antes, mas dentro da classeAquarium.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R- Observe que o
Rfinal está sublinhado em vermelho. Mantenha o ponteiro sobre ele para ver qual é o erro.
- Para fazer uma verificação de
is, você precisa informar ao Kotlin que o tipo é materializado, ou seja, real, e pode ser usado na função. Para fazer isso, coloqueinlinena frente da palavra-chavefunereifiedna frente do tipo genéricoR.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is RDepois que um tipo é reificado, você pode usá-lo como um tipo normal, porque ele é um tipo real após a inclusão inline. Isso significa que você pode fazer verificações de is usando o tipo.
Se você não usar reified aqui, o tipo não será "real" o suficiente para que o Kotlin permita verificações de is. Isso acontece porque os tipos não concretizados só estão disponíveis no momento da compilação e não podem ser usados durante a execução pelo programa. Vamos falar mais sobre isso na próxima seção.
- Transmita
TapWatercomo o tipo. Assim como ao chamar funções genéricas, chame métodos genéricos usando colchetes angulares com o tipo após o nome da função. Execute o programa e observe o resultado.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>()) // true
}⇒ true
Etapa 3: criar funções de extensão
Você também pode usar tipos concretos para funções regulares e de extensão.
- Fora da classe
Aquarium, defina uma função de extensão emWaterSupplychamadaisOfType()que verifica se oWaterSupplytransmitido é de um tipo específico, por exemplo,TapWater.
inline fun <reified T: WaterSupply> WaterSupply.isOfType() = this is T- Chame a função de extensão como um método.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}⇒ true
Com essas funções de extensão, não importa o tipo de Aquarium (Aquarium, TowerTank ou alguma outra subclasse), desde que seja um Aquarium. Usar a sintaxe de projeção de estrela é uma maneira conveniente de especificar uma variedade de correspondências. E quando você usa uma projeção por asterisco, o Kotlin garante que você não faça nada inseguro.
- Para usar uma projeção de estrela, coloque
<*>depois deAquarium. MovahasWaterSupplyOfType()para ser uma função de extensão, porque ela não faz parte da API principal deAquarium.
inline fun <reified R: WaterSupply> Aquarium<*>.hasWaterSupplyOfType() = waterSupply is R- Mude a chamada para
hasWaterSupplyOfType()e execute o programa.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}⇒ true
No exemplo anterior, você precisou marcar o tipo genérico como reified e tornar a função inline, porque o Kotlin precisa saber sobre eles em tempo de execução, não apenas em tempo de compilação.
Todos os tipos genéricos são usados apenas no momento da compilação pelo Kotlin. Isso permite que o compilador garanta que você está fazendo tudo com segurança. No tempo de execução, todos os tipos genéricos são apagados. Por isso, a mensagem de erro anterior sobre a verificação de um tipo apagado.
Acontece que o compilador pode criar o código correto sem manter os tipos genéricos até o tempo de execução. Mas isso significa que, às vezes, você faz algo, como verificações is em tipos genéricos, que o compilador não pode oferecer suporte. Por isso, o Kotlin adicionou tipos materializados ou reais.
Leia mais sobre tipos concretos e eliminação de tipos na documentação do Kotlin.
Esta lição se concentrou em tipos genéricos, que são importantes para tornar o código mais flexível e fácil de reutilizar.
- Crie classes genéricas para tornar o código mais flexível.
- Adicione restrições genéricas para limitar os tipos usados com genéricos.
- Use os tipos
ineoutcom genéricos para oferecer uma verificação de tipo melhor e restringir os tipos transmitidos ou retornados de classes. - Crie funções e métodos genéricos para trabalhar com tipos genéricos. Exemplo:
fun <T: WaterSupply> isWaterClean(aquarium: Aquarium<T>) { ... } - Use funções de extensão genéricas para adicionar funcionalidades não principais a uma classe.
- Às vezes, os tipos concretos são necessários devido ao apagamento de tipos. Os tipos concretos, ao contrário dos tipos genéricos, persistem até o tempo de execução.
- Use a função
check()para verificar se o código está sendo executado como esperado. Por exemplo:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Documentação do Kotlin
Se você quiser mais informações sobre algum tópico deste curso ou se tiver dúvidas, https://kotlinlang.org é o melhor ponto de partida.
- Genéricos
- Restrições genéricas
- Projeções de estrelas
- Tipos
Ineout - Parâmetros concretizados
- Eliminação de tipo
- Função
check()
Tutoriais do Kotlin
O site https://try.kotlinlang.org inclui tutoriais avançados chamados Kotlin Koans, um interpretador baseado na Web e um conjunto completo de documentação de referência com exemplos.
Curso Udacity
Para conferir o curso da Udacity sobre esse tema, consulte Treinamento do Kotlin para programadores (link em inglês).
IntelliJ IDEA
A documentação do IntelliJ IDEA está disponível no site da JetBrains.
Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:
- Atribuir o dever de casa, se necessário.
- Informar aos alunos como enviar deveres de casa.
- Atribuir nota aos deveres de casa.
Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.
Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.
Responda estas perguntas
Pergunta 1
Qual das opções a seguir é a convenção para nomear um tipo genérico?
▢ <Gen>
▢ <Generic>
▢ <T>
▢ <X>
Pergunta 2
Uma restrição nos tipos permitidos para um tipo genérico é chamada de:
▢ uma restrição genérica
▢ uma restrição genérica
▢ desambiguação
▢ um limite de tipo genérico
Pergunta 3
"Materializado" significa:
▢ O impacto real da execução de um objeto foi calculado.
▢ Um índice de entrada restrita foi definido na classe.
▢ O parâmetro de tipo genérico foi transformado em um tipo real.
▢ Um indicador de erro remoto foi acionado.
Siga para a próxima lição:
Para ter uma visão geral do curso, incluindo links para outros codelabs, consulte "Treinamento do Kotlin para programadores: seja bem-vindo ao curso".