Este codelab faz parte do curso Treinamento do Kotlin para programadores. Você aproveitará mais o 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ê conhecerá classes, funções e métodos genéricos e como eles funcionam em Kotlin.
Em vez de criar um único app de exemplo, as lições deste curso foram desenvolvidas para aumentar seu conhecimento, mas são semi-independentes umas das outras para que você possa ler as seções que já conhece. Para conectá-los, muitos dos exemplos usam um tema de aquário. Se você quiser ver a história completa do aquário, confira o curso Bootcamp de Kotlin para programadores na Udacity.
O que você já precisa saber
- 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 do laboratório
- Criar uma classe genérica e adicionar restrições
- Criar tipos
in
eout
- Criar funções genéricas, métodos e funções de extensão
Introdução aos dados genéricos
O Kotlin, assim como muitas linguagens de programação, tem tipos genéricos. Um tipo genérico permite que você torne uma classe genérica e, assim, torne uma classe muito mais flexível.
Imagine que você esteja implementando uma classe MyList
que contém uma lista de itens. Sem os genéricos, seria necessário implementar uma nova versão de MyList
para cada tipo: uma para Double
, uma para String
e outra para Fish
. Com genéricos, você pode tornar a lista genérica para incluir qualquer tipo de objeto. É como transformar o tipo em 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 é a T.
class MyList<T> {
fun get(pos: Int): T {
TODO("implement")
}
fun addItem(item: T) {}
}
É possível referenciar T
como se fosse um tipo normal. O tipo de retorno de get()
é T
, e o parâmetro para addItem()
é do tipo T
. 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ê criará algumas classes para usar na próxima etapa. A subclasse foi abordada em um codelab anterior, mas veja uma breve revisão.
- Para manter o exemplo organizado, crie um novo pacote em src e o chame
generics
. - No pacote generics, crie um novo arquivo
Aquarium.kt
. Isso permite redefinir itens usando os mesmos nomes sem conflitos, de modo que o restante do código deste codelab entra nesse arquivo. - Crie uma hierarquia de tipos de fontes de água. Comece transformando
WaterSupply
em uma classeopen
para que ela possa ser transformada em subclasse. - Adicione um parâmetro booleano
var
,needsProcessing
. Isso cria automaticamente uma propriedade mutável, com um getter e um setter. - Crie uma subclasse
TapWater
que estendaWaterSupply
e transmitatrue
paraneedsProcessing
, porque a água de toque contém aditivos nocivos para o peixe. - No
TapWater
, defina uma função com o nomeaddChemicalCleaners()
que defineneedsProcessing
comofalse
após a limpeza da água. A propriedadeneedsProcessing
pode ser definida a partir deTapWater
, porque épublic
por padrão e acessível para subclasses. Veja o código completo.
package generics
open class WaterSupply(var needsProcessing: Boolean)
class TapWater : WaterSupply(true) {
fun addChemicalCleaners() {
needsProcessing = false
}
}
- Crie mais duas subclasses de
WaterSupply
, com o nomeFishStoreWater
eLakeWater
.FishStoreWater
não precisa de processamento, masLakeWater
precisa ser filtrado com o métodofilter()
. Depois da filtragem, não é necessário processá-la 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 no Kotlin.
Etapa 2: criar uma classe genérica
Nesta etapa, você modificará a classe Aquarium
para oferecer compatibilidade com diferentes tipos de suprimentos de água.
- No Aquarium.kt, defina uma classe
Aquarium
, com<T>
entre os nomes das classes. - Adicione uma propriedade imutável
waterSupply
do tipoT
aAquarium
.
class Aquarium<T>(val waterSupply: T)
- Escreva uma função chamada
genericsExample()
. Ele não faz parte de uma classe, então pode estar no nível superior do arquivo, como a funçãomain()
ou as definições de classe. Na função, crie umaAquarium
e transmita umWaterSupply
. Como o parâmetrowaterSupply
é genérico, é necessário especificar o tipo entre colchetes<>
.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
}
- Em
genericsExample()
, seu código pode acessar owaterSupply
do aquário. Como ele é do tipoTapWater
, é possível chamaraddChemicalCleaners()
sem nenhum tipo de transmissão.
fun genericsExample() {
val aquarium = Aquarium<TapWater>(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Ao criar o objeto
Aquarium
, você pode remover os sinais de "menor que" e "o que" entre eles porque o Kotlin tem inferência de tipo. Portanto, não há motivo para dizerTapWater
duas vezes ao criar a instância. O tipo pode ser inferido pelo argumento paraAquarium
. Ele ainda criará umAquarium
do tipoTapWater
.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
aquarium.waterSupply.addChemicalCleaners()
}
- Para ver o que está acontecendo, exiba
needsProcessing
antes e depois de chamaraddChemicalCleaners()
. Veja 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()
, execute seu programa e observe o resultado.
fun main() {
genericsExample()
}
⇒ water needs processing: true water needs processing: false
Etapa 3: torná-la mais específica
Genérico significa que você pode transmitir quase tudo e, às vezes, isso é um problema. Nesta etapa, você tornará a classe Aquarium
mais específica sobre o que pode ser incluído nela.
- Em
genericsExample()
, crie umAquarium
, transmitindo uma string para owaterSupply
e exiba a propriedadewaterSupply
do aquário.
fun genericsExample() {
val aquarium2 = Aquarium("string")
println(aquarium2.waterSupply)
}
- Execute o programa para observar o resultado.
⇒ string
O resultado é a string que você transmitiu, porque Aquarium
não estabelece limitações em T.
Qualquer tipo, incluindo String
, pode ser transmitido.
- Em
genericsExample()
, crie outroAquarium
, transmitindonull
para owaterSupply
. SewaterSupply
for nulo, exibir"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 você pode transmitir null
ao criar um Aquarium
? Isso é possível porque, por padrão, a T
representa o tipo Any?
anulável, que é o tipo na parte superior da hierarquia de tipos. O exemplo abaixo equivale ao que você digitou anteriormente.
class Aquarium<T: Any?>(val waterSupply: T)
- Para não permitir a transmissão do
null
, torne oT
do tipoAny
explicitamente 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) possa ser transmitido paraT
. SubstituaAny
porWaterSupply
para 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ê aprenderá sobre a função check()
para garantir que seu código tenha o comportamento esperado. A função check()
é uma função de biblioteca padrão no Kotlin. Ele atuará como uma declaração e gerará uma IllegalStateException
se o argumento for avaliado como false
.
- Adicione um método
addWater()
à classeAquarium
para adicionar água, com umcheck()
que certifica-se de 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()
gerará uma exceção.
- Em
genericsExample()
, adicione um código para criar umaAquarium
comLakeWater
e adicione água.
fun genericsExample() {
val aquarium4 = Aquarium(LakeWater())
aquarium4.addWater()
}
- Execute o programa. Você 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 exemplo acima abrange o básico de generalizações. As tarefas a seguir abrangem mais, mas o conceito importante é como declarar e usar uma classe genérica com uma restrição genérica.
Nesta tarefa, você aprenderá sobre tipos de entrada e saída com genéricos. 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.
Ao analisar a classe Aquarium
, você verá que o tipo genérico só é retornado quando recebe a propriedade waterSupply
. Não há métodos que usem um valor do tipo T
como parâmetro, exceto para defini-lo no construtor. O Kotlin permite definir tipos out
para este caso e pode inferir mais informações sobre onde os tipos são seguros. Da mesma forma, você pode definir tipos in
para tipos genéricos que são apenas transmitidos para métodos, não retornados. Isso permite que o Kotlin faça verificações adicionais para a segurança do código.
Os tipos in
e out
são diretivas do sistema de tipos Kotlin. A explicação de todo o sistema de tipos está fora do escopo deste treinamento (isso é bem complexo). No entanto, o compilador sinalizará os tipos que não estão marcados como in
e out
adequadamente. Por isso, você precisa conhecê-los.
Etapa 1: definir um tipo de saída
- Na classe
Aquarium
, mudeT: WaterSupply
para ser do tipoout
.
class Aquarium<out T: WaterSupply>(val waterSupply: T) {
...
}
- No mesmo arquivo, fora da classe, declare uma função
addItemTo()
que espere umAquarium
deWaterSupply
.
fun addItemTo(aquarium: Aquarium<WaterSupply>) = println("item added")
- Chame
addItemTo()
degenericsExample()
e execute seu programa.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
addItemTo(aquarium)
}
⇒ item added
O Kotlin pode garantir que addItemTo()
não faça nada que não seja seguro com o WaterSupply
genérico, porque ele é declarado como um tipo de out
- Se você remover a palavra-chave
out
, o compilador apresentará um erro ao chamaraddItemTo()
, porque o Kotlin não pode garantir que você não esteja fazendo nada de errado com o tipo.
Etapa 2: definir um tipo
O tipo in
é semelhante ao out
, mas para tipos genéricos que são transmitidos apenas para funções, não retornados. Se você tentar retornar um tipo in
, receberá um erro do compilador. Neste exemplo, você definirá um tipo in
como parte de uma interface.
- Em Aquarium.kt, defina uma interface
Cleaner
que usa umT
genérico restrito aWaterSupply
. Como ele é usado apenas como um argumento paraclean()
, é possível torná-lo um parâmetroin
.
interface Cleaner<in T: WaterSupply> {
fun clean(waterSupply: T)
}
- Para usar a interface
Cleaner
, crie uma classeTapWaterCleaner
que implementeCleaner
para limparTapWater
, adicionando produtos químicos.
class TapWaterCleaner : Cleaner<TapWater> {
override fun clean(waterSupply: TapWater) = waterSupply.addChemicalCleaners()
}
- Na classe
Aquarium
, atualize aaddWater()
para usar umCleaner
do tipoT
e 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 código de exemplo da
genericsExample()
para criar umaTapWaterCleaner
, umaAquarium
comTapWater
e adicione água usando um limpador. Ele usará o mais limpo possível.
fun genericsExample() {
val cleaner = TapWaterCleaner()
val aquarium = Aquarium(TapWater())
aquarium.addWater(cleaner)
}
O Kotlin usará as informações do 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 externamente como valores de retorno, os tipos in
podem ser transmitidos para dentro como argumentos.
A documentação aborda mais detalhes sobre o tipo de problemas nos tipos e os tipos de saída.
Nesta tarefa, você aprenderá sobre as funções genéricas e quando usá-las. Normalmente, fazer uma função genérica é uma boa ideia sempre que a função usa um argumento de uma classe com 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 de parâmetro. Uma opção é usarWaterSupply
.
fun isWaterClean(aquarium: Aquarium<WaterSupply>) {
println("aquarium water is clean: ${aquarium.waterSupply.needsProcessing}")
}
No entanto, isso significa que o Aquarium
precisa ter um parâmetro do tipo out
para que ele seja chamado. Às vezes, out
ou in
são muito restritivos, porque você precisa usar um tipo para entrada e saída. Para remover o requisito out
, torne a função genérica.
- Para tornar a função genérica, coloque colchetes angulares após a palavra-chave
fun
com um tipo genéricoT
e qualquer restrição, neste caso,WaterSupply
. MudeAquarium
para ser restringido porT
em 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 pensar nisso.
- Chame a função
isWaterClean()
especificando o tipo entre colchetes angulares logo depois do 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 o resultado.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
isWaterClean(aquarium)
}
⇒ aquarium water is clean: false
Etapa 2: criar um método genérico com um tipo reificado
Você também pode usar funções genéricas para métodos, mesmo em classes que tenham o próprio tipo genérico. Nesta etapa, você adicionará um método genérico ao 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
(T
já é usado) restrito aWaterSupply
e retornatrue
sewaterSupply
for do tipoR
. Essa é a função que você declarou anteriormente, mas dentro da classeAquarium
.
fun <R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
- Observe que o
R
final está sublinhado em vermelho. Mantenha o ponteiro sobre ele para ver qual é o erro. - Para fazer uma verificação de
is
, é necessário informar ao Kotlin que o tipo é reificado ou real e pode ser usado na função. Para fazer isso, coloqueinline
na frente da palavra-chavefun
ereified
na frente do tipo genéricoR
.
inline fun <reified R: WaterSupply> hasWaterSupplyOfType() = waterSupply is R
Depois que um tipo é reificado, você pode usá-lo como um tipo normal, porque é um tipo real depois de in-line. Isso significa que é possível realizar verificações is
usando o tipo.
Se você não usar reified
aqui, o tipo não será "real" suficiente para que o Kotlin permita verificações is
. Isso ocorre porque tipos não reificados estão disponíveis somente no momento da compilação e não podem ser usados no momento da execução pelo programa. Falaremos mais sobre isso na próxima seção.
- Transmita
TapWater
como o tipo. Como para 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
Também é possível usar tipos materializados para funções comuns e de extensão.
- Fora da classe
Aquarium
, defina uma função de extensão noWaterSupply
chamadaisOfType()
, que verifica se oWaterSupply
transmitido é 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 da mesma forma que um método.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.waterSupply.isOfType<TapWater>())
}
⇒ true
Com essas funções de extensão, independentemente do tipo de Aquarium
(Aquarium
, TowerTank
ou alguma outra subclasse), desde que seja um Aquarium
. Usar a sintaxe star-projection é uma maneira conveniente de especificar uma variedade de correspondências. E, ao usar uma projeção em estrela, o Kotlin também garante que você não faça nada que não seja seguro.
- Para usar uma projeção em estrela, coloque
<*>
depois deAquarium
. MovahasWaterSupplyOfType()
para ser uma função de extensão, porque ele 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 seu programa.
fun genericsExample() {
val aquarium = Aquarium(TapWater())
println(aquarium.hasWaterSupplyOfType<TapWater>())
}
⇒ true
No exemplo anterior, você tinha que marcar o tipo genérico como reified
e tornar a função inline
, porque o Kotlin precisa conhecê-los em tempo de execução, não apenas no tempo de compilação.
Todos os tipos genéricos são usados apenas no momento da compilação por Kotlin. Isso permite que o compilador verifique se você está fazendo tudo de forma segura. Durante o tempo de execução, todos os tipos genéricos são apagados. Portanto, a mensagem de erro anterior sobre a verificação de um tipo.
Acontece que o compilador pode criar o código correto sem manter os tipos genéricos até o momento da execução. Mas ela significa que, às vezes, você faz algo, como a is
, verifica tipos genéricos que o compilador não oferece suporte. É por isso que o Kotlin adicionou tipos materializados ou reais.
Saiba mais sobre os tipos materializados e a exclusão de tipos na documentação do Kotlin.
Esta lição se concentra nos genéricos, o que é importante para tornar o código mais flexível e mais fácil de reutilizar.
- Criar 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
in
eout
com genéricos para oferecer uma verificação de tipo melhor para restringir os tipos transmitidos ou retornados de classes. - Criar 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 materializados são necessários devido à exclusão de tipos. Ao contrário dos tipos genéricos, os tipos restaurados permanecem no ambiente de execução.
- Use a função
check()
para verificar se o código está sendo executado como esperado. Exemplo:check(!waterSupply.needsProcessing) { "water supply needs processing first" }
Documentação do Kotlin
Se quiser mais informações sobre qualquer assunto deste curso ou se tiver dificuldades, https://kotlinlang.org é seu melhor ponto de partida.
- Genéricos
- Restrições genéricas
- Projeções com estrelas
- Tipos
In
eout
- Parâmetros corrigidos
- Apagar tipo
- Função
check()
Tutoriais do Kotlin
O site https://try.kotlinlang.org (link em inglês) tem tutoriais elaborados chamados Kotlin Koans, um intérprete baseado na Web, e um conjunto completo de documentação de referência com exemplos.
Curso Udacity
Para ver o curso da Udacity sobre esse assunto, consulte Bootcamp de Kotlin para programadores (link em inglês).
IntelliJ IDEA
A documentação do IntelliJ IDEA (em inglês) pode ser encontrada no site da JetBrains.
Esta seção lista as possíveis atividades para os alunos que estão trabalhando neste codelab como parte de um curso ministrado por um instrutor. Cabe ao instrutor fazer o seguinte:
- Se necessário, atribua o dever de casa.
- Informe aos alunos como enviar o dever de casa.
- Atribua nota aos trabalhos de casa.
Os professores podem usar essas sugestões o quanto quiserem, e eles devem se sentir à vontade para passar o dever de casa como achar adequado.
Se você estiver fazendo este codelab por conta própria, use essas atividades para testar seu conhecimento.
Responda a estas perguntas.
Pergunta 1
Qual das seguintes opções é 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:
▢ uma restrição genérica
▢ uma restrição genérica
▢ desambiguação
▢ um limite de tipo genérico
Pergunta 3
Isso significa o seguinte:
▢ O verdadeiro impacto na execução de um objeto foi calculado.
▢ Um índice de entrada restrito foi definido na classe.
▢ O parâmetro de tipo genérico foi transformado em um tipo real.
▢ Um indicador de erro remoto foi acionado.
Vá para a próxima lição:
Para ter uma visão geral do curso, incluindo links para outros codelabs, consulte "Bootcamp de Kotlin para programadores: bem-vindo ao curso."