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ê criará um programa em Kotlin e aprenderá sobre classes e objetos em Kotlin. Grande parte desse conteúdo já será conhecida se você conhecer outra linguagem orientada a objetos, mas o Kotlin tem algumas diferenças importantes para reduzir a quantidade de código que precisa ser escrita. Você também aprenderá sobre classes abstratas e delegação de interface.
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
- os conceitos básicos do Kotlin, incluindo tipos, operadores e repetições;
- Sintaxe da função do Kotlin
- Noções básicas da programação orientada a objetos
- Princípios básicos de um ambiente de desenvolvimento integrado, como o IntelliJ IDEA ou o Android Studio
O que você vai aprender
- Como criar classes e acessar propriedades em Kotlin.
- Como criar e usar construtores de classe no Kotlin.
- Como criar uma subclasse e como a herança funciona
- Sobre classes abstratas, interfaces e delegação de interface
- Como criar e usar classes de dados.
- Como usar Singletons, enumerações e classes seladas.
Atividades do laboratório
- Criar uma classe com propriedades
- Criar um construtor para uma classe
- Criar uma subclasse
- Examinar exemplos de classes e interfaces abstratas
- Criar uma classe de dados simples
- Saiba mais sobre Singletons, enumerações e classes seladas
Você já conhece os seguintes termos de programação:
- As classes são modelos de objetos. Por exemplo, uma classe
Aquarium
é a planta para criar um objeto de aquário. - Objetos são instâncias de classes. Um objeto de aquário é uma
Aquarium
real. - Propriedades são características de classes, como comprimento, largura e altura de uma
Aquarium
. - Métodos, também chamados de funções de membro, são a funcionalidade da classe. Os métodos são o que você pode fazer com o objeto. Por exemplo, é possível usar
fillWithWater()
um objetoAquarium
. - Uma interface é uma especificação que uma classe pode implementar. Por exemplo, a limpeza é comum para objetos diferentes dos aquários e a limpeza geralmente acontece de maneiras semelhantes para objetos diferentes. Dessa forma, é possível ter uma interface chamada
Clean
que define um métodoclean()
. A classeAquarium
poderia implementar a interfaceClean
para limpar o aquário com uma esponja macia. - Os pacotes são uma forma de agrupar códigos relacionados para manter a organização ou criar uma biblioteca de códigos. Depois que um pacote é criado, você pode importar o conteúdo dele para outro arquivo e reutilizar o código e as classes contidas nele.
Nesta tarefa, você criará um novo pacote e uma classe com algumas propriedades e um método.
Etapa 1: criar um pacote
Os pacotes podem ajudar você a manter seu código organizado.
- No painel Project, no projeto Hello Kotlin, clique com o botão direito do mouse na pasta src.
- Selecione New > Package e chame-o de
example.myapp
.
Etapa 2: criar uma classe com propriedades
As classes são definidas com a palavra-chave class
. Os nomes das classes por convenção começam com uma letra maiúscula.
- Clique com o botão direito no pacote example.myapp.
- Selecione New > Kotlin File / Class.
- Em Kind, selecione Class e nomeie a classe
Aquarium
. O IntelliJ IDEA inclui o nome do pacote no arquivo e cria uma classeAquarium
vazia para você. - Na classe
Aquarium
, defina e inicialize as propriedadesvar
para largura, altura e comprimento (em centímetros). Inicialize as propriedades com valores padrão.
package example.myapp
class Aquarium {
var width: Int = 20
var height: Int = 40
var length: Int = 100
}
Internamente, o Kotlin cria automaticamente getters e setters para as propriedades definidas na classe Aquarium
, para que você possa acessá-las diretamente, por exemplo, myAquarium.length
.
Etapa 3: criar uma função main()
Crie um novo arquivo com o nome main.kt
para manter a função main()
.
- No painel Project à esquerda, clique com o botão direito do mouse no pacote example.myapp.
- Selecione New > Kotlin File / Class.
- Na lista suspensa Kind, mantenha a seleção como File e nomeie o arquivo como
main.kt
. O IntelliJ IDEA inclui o nome do pacote, mas não inclui uma definição de classe para um arquivo, - Defina uma função
buildAquarium()
e, dentro dela, crie uma instância deAquarium
. Para criar uma instância, faça referência à classe como se ela fosse uma função,Aquarium()
. Isso chama o construtor da classe e cria uma instância da classeAquarium
, semelhante ao uso denew
em outras linguagens. - Defina uma função
main()
e chamebuildAquarium()
.
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
Etapa 4: adicionar um método
- Na classe
Aquarium
, adicione um método para exibir as propriedades de dimensão do aquário.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- Em
main.kt
, embuildAquarium()
, chame o métodoprintSize()
emmyAquarium
.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- Execute o programa clicando no triângulo verde ao lado da função
main()
. Observe o resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- Em
buildAquarium()
, adicione o código para definir a altura como 60 e imprima as propriedades de dimensão alteradas.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- Execute o programa e observe o resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
Nesta tarefa, você criará um construtor para a classe e continuará trabalhando com as propriedades.
Etapa 1: criar um construtor
Nesta etapa, você adicionará um construtor à classe Aquarium
criada na primeira tarefa. No exemplo anterior, cada instância de Aquarium
é criada com as mesmas dimensões. É possível alterar as dimensões depois de criá-las, mas seria mais simples criar o tamanho correto para começar.
Em algumas linguagens de programação, o construtor é definido por meio da criação de um método dentro da classe que tenha o mesmo nome dela. Em Kotlin, você define o construtor diretamente na própria declaração de classe, especificando os parâmetros entre parênteses como se a classe fosse um método. Assim como nas funções do Kotlin, esses parâmetros podem incluir valores padrão.
- Na classe
Aquarium
que você criou anteriormente, altere a definição da classe para incluir três parâmetros de construtor com valores padrão paralength
,width
eheight
e atribua-os às propriedades correspondentes.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
// Dimensions in cm
var length: Int = length
var width: Int = width
var height: Int = height
...
}
- A maneira mais compacta do Kotlin é definir as propriedades diretamente com o construtor usando
var
ouval
. O Kotlin também cria os getters e setters automaticamente. Em seguida, remova as definições de propriedade no corpo da classe.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
- Ao criar um objeto
Aquarium
com esse construtor, você não pode especificar argumentos e receber os valores padrão, especificar apenas alguns deles ou especificar todos eles e criar umAquarium
de tamanho totalmente personalizado. Na funçãobuildAquarium()
, teste maneiras diferentes de criar um objetoAquarium
usando os parâmetros nomeados.
fun buildAquarium() {
val aquarium1 = Aquarium()
aquarium1.printSize()
// default height and length
val aquarium2 = Aquarium(width = 25)
aquarium2.printSize()
// default width
val aquarium3 = Aquarium(height = 35, length = 110)
aquarium3.printSize()
// everything custom
val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
aquarium4.printSize()
}
- Execute o programa e observe a saída.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 25 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 110 cm Height: 35 cm Width: 25 cm Length: 110 cm Height: 35 cm
Você não precisou sobrecarregar o construtor e escrever uma versão diferente para cada um desses casos, além de algumas outras para as outras combinações. O Kotlin cria o que é necessário com base nos valores padrão e parâmetros nomeados.
Etapa 2: adicionar blocos de inicialização
Os construtores de exemplo acima apenas declaram propriedades e atribuem o valor de uma expressão a elas. Se o construtor precisar de mais código de inicialização, ele poderá ser colocado em um ou mais blocos init
. Nesta etapa, você adicionará alguns blocos init
à classe Aquarium
.
- Na classe
Aquarium
, adicione um blocoinit
para exibir o objeto e um segundo bloco para exibir o volume em litros.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
init {
println("aquarium initializing")
}
init {
// 1 liter = 1000 cm^3
println("Volume: ${width * length * height / 1000} l")
}
}
- Execute o programa e observe a saída.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm
Os blocos init
são executados na ordem em que aparecem na definição da classe, e todos eles são executados quando o construtor é chamado.
Etapa 3: saber mais sobre construtores secundários
Nesta etapa, você aprenderá sobre construtores secundários e adicionará um à classe. Além de um construtor principal, que pode ter um ou mais blocos init
, uma classe Kotlin também pode ter um ou mais construtores secundários para permitir a sobrecarga de construtores, ou seja, construtores com argumentos diferentes.
- Na classe
Aquarium
, adicione um construtor secundário que aceita vários peixes como argumento, usando a palavra-chaveconstructor
. Crie uma propriedade de tanqueval
para o volume calculado do aquário em litros com base no número de peixes. Considere 2.000 cm^3 de água por peixe, além de um pequeno espaço extra para que a água não derrame.
constructor(numberOfFish: Int) : this() {
// 2,000 cm^3 per fish + extra room so water doesn't spill
val tank = numberOfFish * 2000 * 1.1
}
- No construtor secundário, mantenha o comprimento e a largura (definidos no construtor principal) e calcule a altura necessária para fazer o tanque com o volume fornecido.
// calculate the height needed
height = (tank / (length * width)).toInt()
- Na função
buildAquarium()
, adicione uma chamada para criar umAquarium
usando o novo construtor secundário. Exiba o tamanho e o volume.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
- Execute o programa e observe o resultado.
⇒ aquarium initializing Volume: 80 l Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Observe que o volume é impresso duas vezes, uma vez pelo bloco init
no construtor principal antes da execução do construtor secundário e uma vez pelo código em buildAquarium()
.
Você também poderia incluir a palavra-chave constructor
no construtor principal, mas isso não é necessário na maioria dos casos.
Etapa 4: adicionar um novo getter de propriedade
Nesta etapa, você adicionará um getter de propriedade explícito. O Kotlin define automaticamente getters e setters quando você define propriedades, mas, às vezes, o valor de uma propriedade precisa ser ajustado ou calculado. Por exemplo, acima, você exibiu o volume de Aquarium
. Você pode disponibilizar o volume como uma propriedade definindo uma variável e um getter para ele. Como volume
precisa ser calculado, o getter precisa retornar o valor calculado, o que pode ser feito com uma função de uma linha.
- Na classe
Aquarium
, defina uma propriedadeInt
com o nomevolume
e um métodoget()
para calcular o volume na próxima linha.
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- Remova o bloco
init
que exibe o volume. - Remova o código no
buildAquarium()
que exibe o volume. - No método
printSize()
, adicione uma linha para exibir o volume.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}
- Execute o programa e observe o resultado.
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
As dimensões e o volume são os mesmos de antes, mas o volume só é impresso uma vez depois que o objeto é totalmente inicializado pelo construtor principal e pelo construtor secundário.
Etapa 5: adicionar um setter de propriedade
Nesta etapa, você criará um novo setter de propriedade para o volume.
- Na classe
Aquarium
, alterevolume
paravar
para que ele possa ser definido mais de uma vez. - Adicione um setter para a propriedade
volume
adicionando um métodoset()
abaixo do getter, que recalcula a altura com base na quantidade fornecida de água. Por convenção, o nome do parâmetro setter évalue
, mas você pode alterá-lo, se preferir.
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- Em
buildAquarium()
, adicione o código para definir o volume do aquário como 70 litros. Exiba o novo tamanho.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
- Execute o programa novamente e observe a altura e o volume alterados.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm
Volume: 70 l
Não houve modificadores de visibilidade, como public
ou private
, no código até agora. Isso ocorre porque, por padrão, tudo no Kotlin é público, o que significa que tudo pode ser acessado em qualquer lugar, incluindo classes, métodos, propriedades e variáveis de membros.
No Kotlin, classes, objetos, interfaces, construtores, funções, propriedades e os setters podem ter modificadores de visibilidade:
public
significa visível fora da classe. Tudo é público por padrão, incluindo variáveis e métodos da classe.internal
significa que ele só estará visível nesse módulo. Um módulo é um conjunto de arquivos Kotlin compilados juntos, por exemplo, uma biblioteca ou aplicativo.private
significa que ele só estará visível nessa classe (ou arquivo de origem se você estiver trabalhando com funções).protected
é igual aprivate
, mas também ficará visível para qualquer subclasse.
Consulte Modificadores de visibilidade (link em inglês) na documentação do Kotlin para saber mais.
Variáveis de participante
As propriedades em uma classe ou variáveis de membro são public
por padrão. Se você defini-los com var
, eles serão mutáveis, ou seja, legíveis e graváveis. Se você defini-los com val
, eles serão somente leitura após a inicialização.
Se você quiser uma propriedade que o código possa ler ou gravar, mas o código externo só pode ler, deixe a propriedade e o getter dela como públicas e declare o setter como privado, conforme mostrado abaixo.
var volume: Int
get() = width * height * length / 1000
private set(value) {
height = (value * 1000) / (width * length)
}
Nesta tarefa, você aprenderá como subclasses e herança funcionam no Kotlin. Elas são semelhantes às que você vê em outros idiomas, mas existem algumas diferenças.
No Kotlin, as classes não podem ser transformadas em subclasses por padrão. Da mesma forma, propriedades e variáveis de membros não podem ser substituídas por subclasses (embora possam ser acessadas).
Marque uma classe como open
para que ela seja transformada em subclasse. Da mesma forma, você precisa marcar propriedades e variáveis de membro como open
para substituí-las na subclasse. A palavra-chave open
é obrigatória para evitar o vazamento acidental de detalhes de implementação como parte da interface da classe.
Etapa 1: abrir a classe Aquarium
Nesta etapa, você definirá a classe Aquarium
como open
para substituí-la na próxima etapa.
- Marque a classe
Aquarium
e todas as propriedades dela com a palavra-chaveopen
.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
open var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- Adicione uma propriedade
shape
aberta com o valor"rectangle"
.
open val shape = "rectangle"
- Adicione uma propriedade
water
aberta com um getter que retorne 90% do volume doAquarium
.
open var water: Double = 0.0
get() = volume * 0.9
- Adicione código ao método
printSize()
para exibir a forma e a quantidade de água como uma porcentagem do volume.
fun printSize() {
println(shape)
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
// 1 l = 1000 cm^3
println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
- No
buildAquarium()
, mude o código para criar umaAquarium
comwidth = 25
,length = 25
eheight = 40
.
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- Execute o programa e observe a nova saída.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
Etapa 2: criar uma subclasse
- Crie uma subclasse de
Aquarium
com o nomeTowerTank
, que implementa um tanque de cilindro arredondado em vez de um tanque retangular. Você pode adicionarTowerTank
abaixo deAquarium
porque pode adicionar outra classe no mesmo arquivo da classeAquarium
. - Em
TowerTank
, substitua a propriedadeheight
, que é definida no construtor. Para modificar uma propriedade, use a palavra-chaveoverride
na subclasse.
- Faça com que o construtor para
TowerTank
use umdiameter
. Use adiameter
paralength
ewidth
ao chamar o construtor na superclasseAquarium
.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- Modifique a propriedade de volume para calcular um cilindro. A fórmula para um cilindro é Pi vezes o raio ao quadrado vezes a altura. Você precisa importar a constante
PI
dejava.lang.Math
.
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
- Em
TowerTank
, substitua a propriedadewater
por 80% do volume.
override var water = volume * 0.8
- Modifique o
shape
para que seja"cylinder"
.
override val shape = "cylinder"
- A classe
TowerTank
final ficará parecida com o código abaixo.
Aquarium.kt
:
package example.myapp
import java.lang.Math.PI
... // existing Aquarium class
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
override var volume: Int
// ellipse area = π * r1 * r2
get() = (width/2 * length/2 * height / 1000 * PI).toInt()
set(value) {
height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
}
override var water = volume * 0.8
override val shape = "cylinder"
}
- Em
buildAquarium()
, crie umaTowerTank
com um diâmetro de 25 cm e uma altura de 45 cm. Exiba o tamanho.
main.kt:
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium(width = 25, length = 25, height = 40)
myAquarium.printSize()
val myTower = TowerTank(diameter = 25, height = 40)
myTower.printSize()
}
- Execute o programa e observe o resultado.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full) aquarium initializing cylinder Width: 25 cm Length: 25 cm Height: 40 cm Volume: 18 l Water: 14.4 l (80.0% full)
Às vezes, você quer definir comportamentos ou propriedades comuns a serem compartilhados entre algumas classes relacionadas. O Kotlin oferece duas maneiras de fazer isso: interfaces e classes abstratas. Nesta tarefa, você criará uma classe AquariumFish
abstrata para propriedades comuns a todos os peixes. Você cria uma interface chamada FishAction
para definir um comportamento comum a todos os peixes.
- Nem uma classe abstrata nem uma interface podem ser instanciadas por conta própria, o que significa que não é possível criar objetos desses tipos diretamente.
- As classes abstratas têm construtores.
- As interfaces não podem ter nenhuma lógica de construtor ou armazenar nenhum estado.
Etapa 1. Criar uma classe abstrata
- Em example.myapp, crie um novo arquivo,
AquariumFish.kt
. - Crie uma classe, também chamada de
AquariumFish
, e marque-a comabstract
. - Adicione uma propriedade
String
,color
, e marque-a comabstract
.
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- Crie duas subclasses de
AquariumFish
,Shark
ePlecostomus
. - Como o
color
é abstrato, as subclasses precisam implementá-lo. Deixe oShark
cinza ePlecostomus
dourado.
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- Em main.kt, crie uma função
makeFish()
para testar as classes. Instancie umShark
e umPlecostomus
e exiba a cor de cada um. - Exclua o código de teste anterior em
main()
e adicione uma chamada paramakeFish()
. O código ficará parecido com este:
main.kt
:
package example.myapp
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
println("Plecostomus: ${pleco.color}")
}
fun main () {
makeFish()
}
- Execute o programa e observe o resultado.
⇒ Shark: gray Plecostomus: gold
O diagrama a seguir representa as classes Shark
e Plecostomus
, que têm como subclasse a classe abstrata AquariumFish
.
Etapa 2: Criar uma interface
- Em AquariumFish.kt, crie uma interface chamada
FishAction
com um métodoeat()
.
interface FishAction {
fun eat()
}
- Adicione
FishAction
a cada uma das subclasses e implementeeat()
fazendo com que ele exiba o que o peixe faz.
class Shark: AquariumFish(), FishAction {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
class Plecostomus: AquariumFish(), FishAction {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- Na função
makeFish()
, faça com que cada peixe criado coma algo chamandoeat()
.
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- Execute o programa e observe o resultado.
⇒ Shark: gray hunt and eat fish Plecostomus: gold eat algae
O diagrama a seguir representa a classe Shark
e a classe Plecostomus
, que são compostas e implementam a interface FishAction
.
Quando usar classes abstratas x interfaces
Os exemplos acima são simples, mas quando você tem muitas classes interrelacionadas, as classes e interfaces abstratas podem ajudar a manter seu design mais limpo, mais organizado e mais fácil de manter.
Como mencionado acima, as classes abstratas podem ter construtores e as interfaces não, mas, caso contrário, são muito semelhantes. Quando você deve usar cada um?
Quando você usa interfaces para compor uma classe, a funcionalidade dela é estendida por meio das instâncias de classe que ela contém. A composição tende a facilitar a reutilização e o motivo do código em comparação à herança de uma classe abstrata. Além disso, você pode usar várias interfaces em uma classe, mas só pode criar uma subclasse a partir de uma classe abstrata.
A composição costuma melhorar o encapsulamento, reduzir a acoplamento (interdependência), interfaces mais limpas e criar códigos mais utilizáveis. Por isso, o melhor é usar a composição com interfaces. Por outro lado, a herança de uma classe abstrata tende a ser uma escolha natural para alguns problemas. Por isso, é melhor usar a composição, mas quando a herança faz sentido, o Kotlin também permite isso.
- Use uma interface se você tiver muitos métodos e uma ou duas implementações padrão, como em
AquariumAction
abaixo.
interface AquariumAction {
fun eat()
fun jump()
fun clean()
fun catchFish()
fun swim() {
println("swim")
}
}
- Use uma classe abstrata sempre que não for possível concluí-la. Por exemplo, de volta à classe
AquariumFish
, você pode fazer com queAquariumFish
implementeFishAction
e forneça uma implementação padrão paraeat
, deixandocolor
abstrato, porque não há uma cor padrão para peixes.
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
}
A tarefa anterior introduziu classes abstratas, interfaces e a ideia de composição. A delegação de interface é uma técnica avançada em que os métodos de uma interface são implementados por um objeto auxiliar (ou delegado), que é usado por uma classe. Essa técnica pode ser útil ao usar uma interface em uma série de classes não relacionadas. Você adiciona a funcionalidade de interface necessária a uma classe auxiliar separada, e cada uma das classes usa uma instância da classe auxiliar para implementar a funcionalidade.
Nesta tarefa, você usará a delegação da interface para adicionar funcionalidades a uma classe.
Etapa 1: criar uma nova interface
- No AquariumFish.kt, remova a classe
AquariumFish
. Em vez de herdar da classeAquariumFish
,Plecostomus
eShark
implementarão interfaces para a ação e a cor do peixe. - Crie uma nova interface,
FishColor
, que defina a cor como uma string.
interface FishColor {
val color: String
}
- Mude
Plecostomus
para implementar duas interfaces,FishAction
eFishColor
. Você precisa modificar acolor
daFishColor
e aeat()
daFishAction
.
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- Mude a classe
Shark
para implementar também as duas interfaces,FishAction
eFishColor
, em vez de herdar deAquariumFish
.
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- O código finalizado ficará da seguinte forma:
package example.myapp
interface FishAction {
fun eat()
}
interface FishColor {
val color: String
}
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
Etapa 2: criar uma classe Singleton
Em seguida, implemente a configuração da parte de delegação criando uma classe auxiliar que implemente FishColor
. Você cria uma classe básica chamada GoldColor
que implementa FishColor
. Ela só diz que a cor é dourada.
Não faz sentido utilizar várias instâncias de GoldColor
porque todas fazem exatamente a mesma coisa. Portanto, o Kotlin permite declarar uma classe em que só é possível criar uma instância dela usando a palavra-chave object
em vez de class
. O Kotlin criará essa instância, que será referenciada pelo nome da classe. Dessa forma, todos os outros objetos poderão usar essa única instância. Não há como fazer outras instâncias dessa classe. Se você conhece o padrão Singleton, veja como implementar Singletons em Kotlin.
- Em AquariumFish.kt, crie um objeto para
GoldColor
. Substitua a cor.
object GoldColor : FishColor {
override val color = "gold"
}
Etapa 3: adicionar a delegação de interface ao FishColor
Agora você está pronto para usar a delegação de interface.
- Em AquariumFish.kt, remova a substituição de
color
dePlecostomus
. - Mude a classe
Plecostomus
para ver a cor deGoldColor
. Para fazer isso, adicioneby GoldColor
à declaração de classe, criando a delegação. O que diz é que, em vez de implementarFishColor
, use a implementação fornecida pelaGoldColor
. Portanto, sempre que acolor
for acessada, ela será delegada aGoldColor
.
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
Com a classe como estão, todos os Plecos ficarão dourados, mas esses peixes vêm em diversas cores. Para resolver isso, adicione um parâmetro construtor para a cor com GoldColor
como a cor padrão para Plecostomus
.
- Mude a classe
Plecostomus
para receber uma transmissão emfishColor
com o construtor e defina o padrão comoGoldColor
. Altere a delegação deby GoldColor
paraby fishColor
.
class Plecostomus(fishColor: FishColor = GoldColor): FishAction,
FishColor by fishColor {
override fun eat() {
println("eat algae")
}
}
Etapa 4: adicionar a delegação da interface para FishAction
Da mesma forma, você pode usar a delegação de interface para o FishAction
.
- No AquariumFish.kt, crie uma classe
PrintingFishAction
que implementeFishAction
, que usa umString
,food
e exibe o que o peixe consome.
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- Na classe
Plecostomus
, remova a função de substituiçãoeat()
, porque ela será substituída por uma delegação. - Na declaração de
Plecostomus
, delegueFishAction
aPrintingFishAction
, transmitindo"eat algae"
. - Com toda essa delegação, não há código no corpo da classe
Plecostomus
. Portanto, remova o{}
, porque todas as substituições são processadas pela delegação de interface.
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColor
O diagrama a seguir representa as classes Shark
e Plecostomus
, ambas compostas pelas interfaces PrintingFishAction
e FishColor
, mas delegando a implementação a elas.
A delegação de interface é poderosa e, geralmente, você deve considerar como usá-la sempre que puder usar uma classe abstrata em outra linguagem. Ele permite usar a composição para inserir comportamentos em vez de exigir muitas subclasses, cada uma especializada de uma forma diferente.
Em alguns outros idiomas, uma classe de dados é semelhante a uma struct
. Ela existe principalmente para armazenar alguns dados, mas um objeto de classe de dados ainda é um objeto. Os objetos de classe de dados do Kotlin têm alguns outros benefícios, como utilitários para impressão e cópia. Nesta tarefa, você criará uma classe de dados simples e aprenderá sobre a compatibilidade do Kotlin com essas classes.
Etapa 1: criar uma classe de dados
- Adicione um novo pacote
decor
no pacote example.myapp para armazenar o novo código. Clique com o botão direito do mouse em example.myapp no painel Project e selecione File > New > Package. - No pacote, crie uma nova classe com o nome
Decoration
.
package example.myapp.decor
class Decoration {
}
- Para transformar
Decoration
em uma classe de dados, use a palavra-chavedata
como prefixo na declaração de classe. - Adicione uma propriedade
String
com o nomerocks
para fornecer alguns dados à classe.
data class Decoration(val rocks: String) {
}
- No arquivo, fora da classe, adicione uma função
makeDecorations()
para criar e imprimir uma instância de umDecoration
com"granite"
.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- Adicione uma função
main()
para chamarmakeDecorations()
e executar o programa. Observe o resultado gerado que é criado porque essa é uma classe de dados.
⇒ Decoration(rocks=granite)
- Em
makeDecorations()
, instancie mais dois objetosDecoration
que são ambos "slate" e os exiba.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
- Em
makeDecorations()
, adicione uma instrução que exiba o resultado da comparação dedecoration1
comdecoration2
e uma segunda comparação comdecoration3
comdecoration2
. Use o método equals() fornecido pelas classes de dados.
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- Execute o código.
⇒ Decoration(rocks=granite) Decoration(rocks=slate) Decoration(rocks=slate) false true
Etapa 2: Como desestruturar
Para chegar às propriedades de um objeto de dados e atribuí-las a variáveis, atribua uma de cada vez, desta forma.
val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver
Em vez disso, é possível criar variáveis, uma para cada propriedade, e atribuir o objeto de dados ao grupo de variáveis. O Kotlin coloca o valor da propriedade em cada variável.
val (rock, wood, diver) = decoration
Isso é chamado de desestruturação e é uma abreviação útil. O número de variáveis precisa corresponder ao número de propriedades, e elas são atribuídas na ordem em que são declaradas na classe. Veja um exemplo completo em Decoration.kt.
// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}
fun makeDecorations() {
val d5 = Decoration2("crystal", "wood", "diver")
println(d5)
// Assign all properties to variables.
val (rock, wood, diver) = d5
println(rock)
println(wood)
println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver) crystal wood diver
Se você não precisar de uma ou mais propriedades, pode ignorá-las usando _
em vez de um nome de variável, como mostrado no código abaixo.
val (rock, _, diver) = d5
Nesta tarefa, você aprenderá sobre algumas das classes específicas do Kotlin, incluindo as seguintes:
- Aulas de Singleton
- Enums
- Aulas fechadas
Etapa 1: recuperar classes Singleton
Lembre-se do exemplo anterior com a classe GoldColor
.
object GoldColor : FishColor {
override val color = "gold"
}
Como cada instância de GoldColor
faz a mesma coisa, ela é declarada como object
em vez de class
para torná-la um Singleton. Só pode haver uma instância.
Etapa 2: criar uma enumeração
O Kotlin também é compatível com enumerações, que permitem enumerar e referir-se a elas pelo nome, assim como em outras linguagens. Declare uma enumeração prefixando a declaração com a palavra-chave enum
. Uma declaração de enumeração básica só precisa de uma lista de nomes, mas também é possível definir um ou mais campos associados a cada nome.
- Em Decoration.kt, teste um exemplo de enumeração.
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
Enums são um pouco parecidos com Singletons. Pode haver apenas um e apenas um de cada valor na enumeração. Por exemplo, só pode haver Color.RED
, Color.GREEN
e Color.BLUE
. Neste exemplo, os valores RGB são atribuídos à propriedade rgb
para representar os componentes de cor. Também é possível descobrir o valor ordinal de uma enumeração usando a propriedade ordinal
e o nome dela usando a propriedade name
.
- Teste outro exemplo de enumeração.
enum class Direction(val degrees: Int) {
NORTH(0), SOUTH(180), EAST(90), WEST(270)
}
fun main() {
println(Direction.EAST.name)
println(Direction.EAST.ordinal)
println(Direction.EAST.degrees)
}
⇒ EAST 2 90
Etapa 3: criar uma classe selada
Uma classe selada pode ser uma subclasse, mas somente no arquivo em que é declarada. Se você tentar criar uma subclasse em um arquivo diferente, receberá uma mensagem de erro.
Como as classes e subclasses ficam no mesmo arquivo, o Kotlin conhecerá todas as subclasses estaticamente. Ou seja, no tempo de compilação, o compilador vê todas as classes e subclasses e sabe que todas elas são para que ele possa fazer verificações adicionais para você.
- No AquariumFish.kt, teste um exemplo de classe fechada, com o tema aquático.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()
fun matchSeal(seal: Seal): String {
return when(seal) {
is Walrus -> "walrus"
is SeaLion -> "sea lion"
}
}
A classe Seal
não pode ser uma subclasse em outro arquivo. Se você quiser adicionar mais tipos de Seal
, será necessário adicioná-los no mesmo arquivo. Isso torna as classes seladas uma maneira segura de representar um número fixo de tipos. Por exemplo, classes seladas são ótimas para retornar um sucesso ou um erro em uma API de rede.
Esta lição abordava muitos aspectos. Embora grande parte dele deva ser conhecida por outras linguagens de programação orientadas a objetos, o Kotlin adiciona alguns recursos para manter o código conciso e legível.
Classes e construtores
- Defina uma classe no Kotlin usando
class
. - O Kotlin cria automaticamente setters e getters para propriedades.
- Defina o construtor principal diretamente na definição da classe. Exemplo:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
- Se um construtor principal precisar de mais código, grave-o em um ou mais blocos
init
. - Uma classe pode definir um ou mais construtores secundários usando
constructor
. No entanto, o estilo Kotlin é usar uma função de fábrica.
Modificadores e subclasses de visibilidade
- Todas as classes e funções em Kotlin são
public
por padrão, mas você pode usar modificadores para mudar a visibilidade parainternal
,private
ouprotected
. - Para criar uma subclasse, a classe pai precisa ser marcada como
open
. - Para substituir os métodos e propriedades em uma subclasse, eles precisam ser marcados como
open
na classe pai. - Uma classe selada só pode ser transformada em subclasse no mesmo arquivo em que é definida. Faça uma classe selada adicionando o prefixo
sealed
à declaração.
Classes de dados, Singletons e enumerações
- Faça uma classe de dados prefixando a declaração com
data
. - Desestruturação é um atalho para atribuir as propriedades de um objeto
data
para separar variáveis. - Crie uma classe Singleton usando
object
em vez declass
. - Definir uma enumeração usando
enum class
Classes, interfaces e delegação abstratas
- Classes e interfaces abstratas são duas maneiras de compartilhar comportamentos comuns entre classes.
- Uma classe abstrata define propriedades e comportamento, mas deixa a implementação para subclasses.
- Uma interface define o comportamento e pode fornecer implementações padrão para alguns ou todos os comportamentos.
- Quando você usa interfaces para compor uma classe, a funcionalidade dela é estendida por meio das instâncias de classe que ela contém.
- A delegação de interface usa a composição, mas também delega a implementação às classes de interface.
- A composição é uma forma eficiente de adicionar funcionalidade a uma classe usando a delegação de interface. Em geral, a composição é preferível, mas a herança de uma classe abstrata é mais adequada para alguns problemas.
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.
- Classes e herança (link em inglês)
- Construtores (link em inglês)
- Funções de fábrica
- Propriedades e campos
- Modificadores de visibilidade
- Classes abstratas
- Interfaces
- Delegação
- Classes de dados
- Igualdade
- Desestruturação
- Declarações de objetos
- Classes de enumeração
- Classes seladas
- Como lidar com erros opcionais usando classes seladas do Kotlin
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
As classes têm um método especial que serve como modelo para a criação de objetos dessa classe. Como se chama o método?
▢ Um criador
▢ Um instanciador
▢ Um construtor
▢ Um modelo
Pergunta 2
Qual das seguintes afirmações sobre interfaces e classes abstratas NÃO está correta?
▢ As classes abstratas podem ter construtores.
▢ As interfaces não podem ter construtores.
▢ As interfaces e classes abstratas podem ser instanciadas diretamente.
▢ As propriedades abstratas precisam ser implementadas por subclasses da classe abstrata.
Pergunta 3
Qual das seguintes opções NÃO é um modificador de visibilidade do Kotlin para propriedades, métodos etc.?
▢ internal
▢ nosubclass
▢ protected
▢ private
Pergunta 4
Considere esta classe de dados:data class Fish(val name: String, val species:String, val colors:String)
Qual das opções a seguir NÃO é um código válido para criar e desestruturar um objeto Fish
?
▢ val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")
▢ val (name2, _, colors2) = Fish("Bitey", "shark", "gray")
▢ val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")
▢ val (name4, species4, colors4) = Fish("Harry", "halibut")
Pergunta 5
Digamos que você tenha um zoológico com muitos animais que precisam ser cuidadodos. Qual alternativa NÃO faz parte da implementação de cuidados?
▢ interface
: tipos de alimentos que os animais comem.
▢ Uma classe abstract Caretaker
que permite criar diferentes tipos de cuidador.
▢ Uma interface
para fornecer água limpa a um animal.
▢ Uma classe data
para uma entrada em uma programação de alimentação.
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."