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 criar um programa e aprender sobre classes e objetos no Kotlin. Grande parte desse conteúdo será familiar se você conhecer outra linguagem orientada a objetos, mas o Kotlin tem algumas diferenças importantes para reduzir a quantidade de código que você precisa escrever. Você também vai aprender sobre classes abstratas e delegação de interface.
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
- Os conceitos básicos do Kotlin, incluindo tipos, operadores e loops
- Sintaxe de função do Kotlin
- Conceitos básicos da programação orientada a objetos
- Os 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 no 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, enums e classes seladas
Atividades deste laboratório
- Criar uma classe com propriedades
- Criar um construtor para uma classe
- Criar uma subclasse
- Analisar exemplos de classes abstratas e interfaces
- Criar uma classe de dados simples
- Saiba mais sobre singletons, enums e classes seladas
Você já precisa conhecer os seguintes termos de programação:
- As classes são plantas 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 é um
Aquarium
real. - Propriedades são características de classes, como comprimento, largura e altura de um
Aquarium
. - Os 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
fillWithWater()
um objetoAquarium
. - Uma interface é uma especificação que uma classe pode implementar. Por exemplo, a limpeza é comum a objetos que não são aquários, e geralmente acontece de maneira semelhante para diferentes objetos. Assim, você pode ter uma interface chamada
Clean
que define um métodoclean()
. A classeAquarium
pode implementar a interfaceClean
para limpar o aquário com uma esponja macia. - Pacotes são uma maneira de agrupar códigos relacionados para manter tudo organizado ou criar uma biblioteca de código. Depois que um pacote é criado, é possível importar o conteúdo dele para outro arquivo e reutilizar o código e as classes nele.
Nesta tarefa, você vai criar um novo pacote e uma classe com algumas propriedades e um método.
Etapa 1: criar um pacote
Os pacotes ajudam a manter seu código organizado.
- No painel Projeto, em Hello Kotlin, clique com o botão direito do mouse na pasta src.
- Selecione New > Package e chame de
example.myapp
.
Etapa 2: criar uma classe com propriedades
As classes são definidas com a palavra-chave class
, e os nomes de classe, por convenção, começam com uma letra maiúscula.
- Clique com o botão direito do mouse no pacote example.myapp.
- Selecione New > Kotlin File / Class.
- Em Kind, selecione Class e nomeie a classe como
Aquarium
. O IntelliJ IDEA inclui o nome do pacote no arquivo e cria uma classeAquarium
vazia para você. - Dentro da 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
. Assim, é possível acessar as propriedades diretamente, por exemplo, myAquarium.length
.
Etapa 3: criar uma função main()
Crie um arquivo chamado main.kt
para armazenar a função main()
.
- No painel Projeto à esquerda, clique com o botão direito do mouse no pacote example.myapp.
- Selecione New > Kotlin File / Class.
- No menu suspenso Tipo, mantenha a seleção como Arquivo 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 crie uma instância deAquarium
dentro dela. Para criar uma instância, faça referência à classe como se 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 imprimir 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()
}
- Para executar o programa, clique 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 imprimir as propriedades de dimensão alteradas.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- Execute o programa e observe a saída.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
Nesta tarefa, você vai criar um construtor para a classe e continuar trabalhando com propriedades.
Etapa 1: criar um construtor
Nesta etapa, você adiciona um construtor à classe Aquarium
criada na primeira tarefa. No exemplo anterior, todas as instâncias de Aquarium
são criadas com as mesmas dimensões. É possível mudar as dimensões depois que ela é criada definindo as propriedades, mas seria mais simples criar com o tamanho correto desde o início.
Em algumas linguagens de programação, o construtor é definido criando um método na classe com o mesmo nome dela. Em Kotlin, você define o construtor diretamente na declaração de classe, especificando os parâmetros entre parênteses como se a classe fosse um método. Assim como nas funções em Kotlin, esses parâmetros podem incluir valores padrão.
- Na classe
Aquarium
criada anteriormente, mude 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ê pode não especificar argumentos e receber os valores padrão, especificar apenas alguns deles ou especificar todos e criar umAquarium
de tamanho totalmente personalizado. Na funçãobuildAquarium()
, teste diferentes maneiras de criar um objetoAquarium
usando 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
Não foi preciso sobrecarregar o construtor e escrever uma versão diferente para cada um desses casos (além de mais alguns para as outras combinações). O Kotlin cria o que é necessário com base nos valores padrão e nos 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ê vai adicionar alguns blocos init
à classe Aquarium
.
- Na classe
Aquarium
, adicione um blocoinit
para mostrar que o objeto está sendo inicializado e um segundo bloco para mostrar 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: saiba mais sobre construtores secundários
Nesta etapa, você vai aprender sobre construtores secundários e adicionar um à sua 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 receba um número de peixes como argumento, usando a palavra-chaveconstructor
. Crie uma propriedade deval
para o volume calculado do aquário em litros com base no número de peixes. Considere 2 litros (2.000 cm³) de água por peixe, além de um pouco mais de espaço para que a água não transborde.
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) iguais e calcule a altura necessária para que o tanque tenha o volume especificado.
// 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. Mostre 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 a saída.
⇒ aquarium initializing Volume: 80 l Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
O volume é impresso duas vezes: uma pelo bloco init
no construtor principal antes da execução do construtor secundário e outra pelo código em buildAquarium()
.
Você também poderia ter incluído 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ê adiciona 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ê imprimiu o volume do Aquarium
. É possível disponibilizar o volume como uma propriedade definindo uma variável e um getter para ela. 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
chamadavolume
e um métodoget()
que calcula o volume na próxima linha.
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- Remova o bloco
init
que imprime o volume. - Remova o código em
buildAquarium()
que imprime o volume. - No método
printSize()
, adicione uma linha para imprimir 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 a saída.
⇒ 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 secundário.
Etapa 5: adicionar um setter de propriedade
Nesta etapa, você vai criar um novo setter de propriedade para o volume.
- Na classe
Aquarium
, mudevolume
para umvar
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 de água fornecida. Por convenção, o nome do parâmetro setter évalue
, mas você pode mudar 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 em 70 litros. Mostre 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
Até agora, não houve modificadores de visibilidade, como public
ou private
, no código. Isso acontece 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 membro.
Em Kotlin, classes, objetos, interfaces, construtores, funções, propriedades e respectivos 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ó vai ficar visível dentro desse módulo. Um módulo é um conjunto de arquivos Kotlin compilados juntos, por exemplo, uma biblioteca ou um aplicativo.private
significa que ele só vai ficar visível nessa classe (ou arquivo de origem, se você estiver trabalhando com funções).protected
é igual aprivate
, mas também fica visível para todas as subclasses.
Consulte Modificadores de visibilidade na documentação do Kotlin para mais informações.
Variáveis de membro
As propriedades em uma classe, ou variáveis de membro, são public
por padrão. Se você as definir com var
, elas serão mutáveis, ou seja, legíveis e graváveis. Se você os definir com val
, eles serão somente leitura após a inicialização.
Se você quiser uma propriedade que seu código possa ler ou gravar, mas que um código externo só possa ler, deixe a propriedade e o getter como públicos 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ê vai aprender como as subclasses e a herança funcionam no Kotlin. Elas são parecidas com o que você já viu em outros idiomas, mas há algumas diferenças.
Em Kotlin, por padrão, as classes não podem ser transformadas em subclasses. Da mesma forma, propriedades e variáveis de membro não podem ser substituídas por subclasses (embora possam ser acessadas).
É necessário marcar uma classe como open
para permitir 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ê vai tornar a classe Aquarium
open
para poder substituir 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 imprimir 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)")
}
- Em
buildAquarium()
, mude o código para criar umAquarium
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
chamadaTowerTank
, que implementa um tanque cilíndrico arredondado em vez de um tanque retangular. Você pode adicionarTowerTank
abaixo deAquarium
, porque é possível adicionar outra classe no mesmo arquivo da classeAquarium
. - Em
TowerTank
, substitua a propriedadeheight
, que é definida no construtor. Para substituir uma propriedade, use a palavra-chaveoverride
na subclasse.
- Faça com que o construtor de
TowerTank
use umdiameter
. Use odiameter
paralength
ewidth
ao chamar o construtor na superclasseAquarium
.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- Substitua a propriedade de volume para calcular um cilindro. A fórmula para um cilindro é pi vezes o raio ao quadrado vezes a altura. É necessário 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
para que ela seja 80% do volume.
override var water = volume * 0.8
- Modifique o
shape
para que seja"cylinder"
.
override val shape = "cylinder"
- A classe
TowerTank
final vai 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 umTowerTank
com diâmetro de 25 cm e altura de 45 cm. Mostre 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 a saída.
⇒ 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 um comportamento ou propriedades comuns para serem compartilhados entre algumas classes relacionadas. O Kotlin oferece duas maneiras de fazer isso: interfaces e classes abstratas. Nesta tarefa, você vai criar uma classe abstrata AquariumFish
para propriedades comuns a todos os peixes. Você cria uma interface chamada FishAction
para definir um comportamento comum a todos os peixes.
- Não é possível instanciar uma classe abstrata nem uma interface 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 lógica de construtor nem armazenar estados.
Etapa 1. Criar uma classe abstrata
- Em example.myapp, crie um arquivo chamado
AquariumFish.kt
. - Crie uma classe, também chamada
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
color
é abstrato, as subclasses precisam implementá-lo. DeixeShark
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 suas classes. Instancie umShark
e umPlecostomus
e imprima a cor de cada um. - Exclua o código de teste anterior em
main()
e adicione uma chamada paramakeFish()
. O código vai ficar assim:
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 a saída.
⇒ Shark: gray Plecostomus: gold
O diagrama a seguir representa as classes Shark
e Plecostomus
, que são subclasses da 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()
para que ele imprima 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 a saída.
⇒ Shark: gray hunt and eat fish Plecostomus: gold eat algae
O diagrama a seguir representa as classes Shark
e Plecostomus
, ambas compostas e implementando a interface FishAction
.
Quando usar classes abstratas em vez de interfaces
Os exemplos acima são simples, mas quando você tem muitas classes inter-relacionadas, as classes abstratas e as interfaces podem ajudar a manter o design mais limpo, organizado e fácil de manter.
Como observado acima, as classes abstratas podem ter construtores, e as interfaces não, mas, de outra forma, elas são muito semelhantes. Então, quando usar cada uma?
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 raciocínio sobre o código do que a herança de uma classe abstrata. Além disso, é possível usar várias interfaces em uma classe, mas só é possível criar subclasses de uma classe abstrata.
A composição geralmente leva a um melhor encapsulamento, menor acoplamento (interdependência), interfaces mais limpas e código mais utilizável. Por esses motivos, usar composição com interfaces é o design preferido. Por outro lado, a herança de uma classe abstrata tende a ser uma opção natural para alguns problemas. Portanto, prefira a composição, mas quando a herança fizer sentido, o Kotlin também permite isso.
- Use uma interface se você tiver muitos métodos e uma ou duas implementações padrão, por exemplo, 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 concluir uma classe. Por exemplo, voltando à classe
AquariumFish
, é possível fazer com que todos osAquariumFish
implementemFishAction
e fornecer 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 apresentou 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 quando você usa uma interface em uma série de classes não relacionadas: adicione 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ê vai usar a delegação de interface para adicionar funcionalidade a uma classe.
Etapa 1: criar uma nova interface
- Em AquariumFish.kt, remova a classe
AquariumFish
. Em vez de herdar da classeAquariumFish
,Plecostomus
eShark
vão implementar interfaces para a ação do peixe e a cor dele. - Crie uma nova interface,
FishColor
, que define a cor como uma string.
interface FishColor {
val color: String
}
- Mude
Plecostomus
para implementar duas interfaces,FishAction
e umFishColor
. É necessário substituir ocolor
deFishColor
e oeat()
deFishAction
.
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- Mude a classe
Shark
para também implementar 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 vai ficar assim:
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 implementa FishColor
. Você cria uma classe básica chamada GoldColor
que implementa FishColor
. Tudo o que ela faz é dizer que a cor é dourada.
Não faz sentido criar várias instâncias de GoldColor
, porque todas fariam exatamente a mesma coisa. Assim, o Kotlin permite declarar uma classe em que só é possível criar uma instância usando a palavra-chave object
em vez de class
. O Kotlin vai criar essa instância, que é referenciada pelo nome da classe. Então, todos os outros objetos podem usar apenas essa instância. Não há como criar outras instâncias dessa classe. Se você conhece o padrão singleton, saiba 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 delegação de interface para FishColor
Agora você pode usar a delegação de interface.
- Em AquariumFish.kt, remova a substituição de
color
dePlecostomus
. - Mude a classe
Plecostomus
para receber a cor deGoldColor
. Para fazer isso, adicioneby GoldColor
à declaração de classe, criando a delegação. Isso significa que, em vez de implementarFishColor
, use a implementação fornecida porGoldColor
. Assim, sempre quecolor
é acessado, ele é delegado aGoldColor
.
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
Com a classe como está, todos os Plecos serão dourados, mas esses peixes vêm em muitas cores. Para resolver isso, adicione um parâmetro de construtor para a cor com GoldColor
como a cor padrão de Plecostomus
.
- Mude a classe
Plecostomus
para receber umafishColor
transmitida com o construtor e defina o padrão comoGoldColor
. Mude 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 delegação de interface para FishAction
Da mesma forma, é possível usar a delegação de interface para o FishAction
.
- Em AquariumFish.kt, crie uma classe
PrintingFishAction
que implementeFishAction
, que usa umString
,food
e imprime o que o peixe come.
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
paraPrintingFishAction
, 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 você geralmente deve considerar como usá-la sempre que usar uma classe abstrata em outra linguagem. Ela permite usar a composição para conectar comportamentos, em vez de exigir muitas subclasses, cada uma especializada de uma maneira diferente.
Uma classe de dados é semelhante a uma struct
em algumas outras linguagens. 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 benefícios extras, como utilitários para impressão e cópia. Nesta tarefa, você vai criar uma classe de dados simples e aprender sobre o suporte que o Kotlin oferece para elas.
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 chamada
Decoration
.
package example.myapp.decor
class Decoration {
}
- Para transformar
Decoration
em uma classe de dados, adicione a palavra-chavedata
como prefixo à declaração da classe. - Adicione uma propriedade
String
chamadarocks
para dar 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 execute o programa. Observe a saída sensata criada porque esta é uma classe de dados.
⇒ Decoration(rocks=granite)
- Em
makeDecorations()
, crie mais dois objetosDecoration
que sejam "slate" e imprima-os.
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 de impressão que mostre o resultado da comparação dedecoration1
comdecoration2
e outra que comparedecoration3
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. Usar desestruturação
Para acessar as propriedades de um objeto de dados e atribuí-las a variáveis, você pode fazer isso uma de cada vez, assim:
val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver
Em vez disso, você pode 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 de propriedades, e as variáveis são atribuídas na ordem em que são declaradas na classe. Confira um exemplo completo que você pode testar 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, pule-as usando _
em vez de um nome de variável, conforme mostrado no código abaixo.
val (rock, _, diver) = d5
Nesta tarefa, você vai aprender sobre algumas das classes de propósito especial em Kotlin, incluindo:
- Classes singleton
- Enums
- Classes seladas
Etapa 1: relembrar classes singleton
Relembre o exemplo anterior com a classe GoldColor
.
object GoldColor : FishColor {
override val color = "gold"
}
Como todas as instâncias de GoldColor
fazem a mesma coisa, ela é declarada como um object
em vez de um class
para se tornar um singleton. Só pode haver uma instância dele.
Etapa 2: criar uma enumeração
O Kotlin também oferece suporte a enums, que permitem enumerar algo e se referir a ele por nome, assim como em outras linguagens. Declare uma enumeração adicionando a palavra-chave enum
como prefixo à declaração. Uma declaração de enumeração básica só precisa de uma lista de nomes, mas você também pode definir um ou mais campos associados a cada nome.
- Em Decoration.kt, confira um exemplo de enumeração.
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
Enums são um pouco como singletons: só pode haver um, e apenas um de cada valor na enumeração. Por exemplo, só pode haver um Color.RED
, um Color.GREEN
e um Color.BLUE
. Neste exemplo, os valores RGB são atribuídos à propriedade rgb
para representar os componentes de cor. Também é possível receber 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 sealed
Uma classe selada é uma classe que pode ser subclassificada, mas apenas dentro do arquivo em que é declarada. Se você tentar criar uma subclasse em um arquivo diferente, vai receber um erro.
Como as classes e subclasses estão no mesmo arquivo, o Kotlin conhece todas as subclasses de forma estática. Ou seja, no momento da compilação, o compilador vê todas as classes e subclasses e sabe que são todas elas. Assim, ele pode fazer verificações extras para você.
- Em AquariumFish.kt, confira um exemplo de classe selada, mantendo 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 subclassificada em outro arquivo. Se você quiser adicionar mais tipos de Seal
, faça isso no mesmo arquivo. Isso torna as classes seladas uma maneira segura de representar um número fixo de tipos. Por exemplo, classes sealed são ótimas para retornar sucesso ou erro de uma API de rede.
Esta lição abordou muita coisa. Embora grande parte seja familiar de 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 em 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 código adicional, escreva-o em um ou mais blocos
init
. - Uma classe pode definir um ou mais construtores secundários usando
constructor
, mas o estilo Kotlin é usar uma função de fábrica.
Modificadores de visibilidade e subclasses
- Todas as classes e funções no 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 mãe precisa ser marcada como
open
. - Para substituir métodos e propriedades em uma subclasse, eles precisam ser marcados como
open
na classe pai. - Uma classe sealed só pode ser subclassificada no mesmo arquivo em que é definida. Crie uma classe selada adicionando
sealed
como prefixo à declaração.
Classes de dados, singletons e enums
- Crie uma classe de dados adicionando
data
como prefixo à declaração. - A desestruturação é uma abreviação para atribuir as propriedades de um objeto
data
a variáveis separadas. - Crie uma classe singleton usando
object
em vez declass
. - Defina uma enumeração usando
enum class
.
Classes abstratas, interfaces e delegação
- Classes abstratas e interfaces 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 parte ou todo o comportamento.
- 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 composição, mas também delega a implementação às classes de interface.
- A composição é uma maneira 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 você quiser mais informações sobre algum tópico deste curso ou se tiver dúvidas, https://kotlinlang.org é o 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 processar erros opcionais usando classes seladas do Kotlin
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
As classes têm um método especial que serve como modelo para criar objetos dessa classe. Qual é o nome do método?
▢ Um builder
▢ Um instanciador
▢ Um construtor
▢ Um blueprint
Pergunta 2
Qual das seguintes afirmações sobre interfaces e classes abstratas NÃO está correta?
▢ Classes abstratas podem ter construtores.
▢ As interfaces não podem ter construtores.
▢ Interfaces e classes abstratas podem ser instanciadas diretamente.
▢ As propriedades abstratas precisam ser implementadas por subclasses da classe abstrata.
Pergunta 3
Qual das opções a seguir 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 de cuidados. Qual das opções a seguir NÃO faz parte da implementação da proteção?
▢ Uma interface
para diferentes tipos de alimentos que os animais comem.
▢ Uma classe abstract Caretaker
em que é possível criar diferentes tipos de responsáveis.
▢ Um interface
por dar água limpa a um animal.
▢ Uma classe data
para uma entrada em uma programação de alimentação.
Acesse 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".