Questo codelab fa parte del corso Kotlin Bootcamp for Programmers. Per ottenere il massimo valore da questo corso, ti consigliamo di seguire le codelab in sequenza. A seconda delle tue conoscenze, potresti riuscire a leggere rapidamente alcune sezioni. Questo corso è rivolto ai programmatori che conoscono un linguaggio orientato agli oggetti e vogliono imparare Kotlin.
Introduzione
In questo codelab, creerai un programma Kotlin e imparerai a conoscere le classi e gli oggetti in Kotlin. Gran parte di questi contenuti ti risulterà familiare se conosci un altro linguaggio orientato agli oggetti, ma Kotlin presenta alcune differenze importanti per ridurre la quantità di codice da scrivere. Scopri anche le classi astratte e la delega di interfacce.
Anziché creare una singola app di esempio, le lezioni di questo corso sono progettate per sviluppare le tue conoscenze, ma sono semi-indipendenti l'una dall'altra, in modo che tu possa scorrere rapidamente le sezioni che conosci. Per collegarli, molti esempi utilizzano un tema acquatico. Se vuoi scoprire tutta la storia dell'acquario, dai un'occhiata al corso Kotlin Bootcamp for Programmers di Udacity.
Cosa devi già sapere
- Le basi di Kotlin, inclusi tipi, operatori e cicli
- Sintassi delle funzioni di Kotlin
- Nozioni di base della programmazione orientata agli oggetti
- Le nozioni di base di un IDE come IntelliJ IDEA o Android Studio
Obiettivi didattici
- Come creare classi e accedere alle proprietà in Kotlin
- Come creare e utilizzare i costruttori di classi in Kotlin
- Come creare una sottoclasse e come funziona l'ereditarietà
- Informazioni su classi astratte, interfacce e delega di interfacce
- Come creare e utilizzare le classi di dati
- Come utilizzare singleton, enum e classi sigillate
In questo lab proverai a:
- Creare una classe con proprietà
- Creare un costruttore per una classe
- Creare una sottoclasse
- Esaminare esempi di classi astratte e interfacce
- Creare una semplice classe di dati
- Scopri di più su singleton, enumerazioni e classi sigillate
Dovresti già conoscere i seguenti termini di programmazione:
- Le classi sono progetti per gli oggetti. Ad esempio, una classe
Aquarium
è il progetto per creare un oggetto acquario. - Gli oggetti sono istanze di classi; un oggetto acquario è un
Aquarium
reale. - Le proprietà sono caratteristiche delle classi, come la lunghezza, la larghezza e l'altezza di un
Aquarium
. - I metodi, chiamati anche funzioni membro, sono la funzionalità della classe. I metodi sono le azioni che puoi "fare" con l'oggetto. Ad esempio, puoi
fillWithWater()
un oggettoAquarium
. - Un'interfaccia è una specifica che una classe può implementare. Ad esempio, la pulizia è comune a oggetti diversi dagli acquari e in genere avviene in modo simile per oggetti diversi. Quindi, potresti avere un'interfaccia chiamata
Clean
che definisce un metodoclean()
. La classeAquarium
potrebbe implementare l'interfacciaClean
per pulire l'acquario con una spugna morbida. - I pacchetti sono un modo per raggruppare il codice correlato per mantenerlo organizzato o per creare una libreria di codice. Una volta creato un pacchetto, puoi importarne i contenuti in un altro file e riutilizzare il codice e le classi.
In questa attività, creerai un nuovo pacchetto e una classe con alcune proprietà e un metodo.
Passaggio 1: crea un pacchetto
I pacchetti possono aiutarti a mantenere il codice organizzato.
- Nel riquadro Progetto, sotto il progetto Hello Kotlin, fai clic con il tasto destro del mouse sulla cartella src.
- Seleziona Nuovo > Pacchetto e chiamalo
example.myapp
.
Passaggio 2: crea una classe con proprietà
Le classi sono definite con la parola chiave class
e i nomi delle classi per convenzione iniziano con una lettera maiuscola.
- Fai clic con il tasto destro del mouse sul pacchetto example.myapp.
- Seleziona Nuovo > File / classe Kotlin.
- In Tipo, seleziona Corso e assegna al corso il nome
Aquarium
. IntelliJ IDEA include il nome del pacchetto nel file e crea una classeAquarium
vuota. - All'interno della classe
Aquarium
, definisci e inizializza le proprietàvar
per la larghezza, l'altezza e la lunghezza (in centimetri). Inizializza le proprietà con i valori predefiniti.
package example.myapp
class Aquarium {
var width: Int = 20
var height: Int = 40
var length: Int = 100
}
Kotlin crea automaticamente getter e setter per le proprietà definite nella classe Aquarium
, in modo da poter accedere direttamente alle proprietà, ad esempio myAquarium.length
.
Passaggio 3: crea una funzione main()
Crea un nuovo file denominato main.kt
per contenere la funzione main()
.
- Nel riquadro Progetto a sinistra, fai clic con il tasto destro del mouse sul pacchetto example.myapp.
- Seleziona Nuovo > File / classe Kotlin.
- Nel menu a discesa Tipo, mantieni la selezione File e assegna al file il nome
main.kt
. IntelliJ IDEA include il nome del pacchetto, ma non una definizione di classe per un file. - Definisci una funzione
buildAquarium()
e al suo interno crea un'istanza diAquarium
. Per creare un'istanza, fai riferimento alla classe come se fosse una funzione,Aquarium()
. Viene chiamato il costruttore della classe e viene creata un'istanza della classeAquarium
, in modo simile all'utilizzo dinew
in altri linguaggi. - Definisci una funzione
main()
e chiamabuildAquarium()
.
package example.myapp
fun buildAquarium() {
val myAquarium = Aquarium()
}
fun main() {
buildAquarium()
}
Passaggio 4: aggiungi un metodo
- Nella classe
Aquarium
, aggiungi un metodo per stampare le proprietà delle dimensioni dell'acquario.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm ")
}
- In
main.kt
, inbuildAquarium()
, chiama il metodoprintSize()
sumyAquarium
.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
}
- Esegui il programma facendo clic sul triangolo verde accanto alla funzione
main()
. Osserva il risultato.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm
- In
buildAquarium()
, aggiungi il codice per impostare l'altezza su 60 e stampare le proprietà delle dimensioni modificate.
fun buildAquarium() {
val myAquarium = Aquarium()
myAquarium.printSize()
myAquarium.height = 60
myAquarium.printSize()
}
- Esegui il programma e osserva l'output.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm Width: 20 cm Length: 100 cm Height: 60 cm
In questa attività, creerai un costruttore per la classe e continuerai a lavorare con le proprietà.
Passaggio 1: crea un costruttore
In questo passaggio, aggiungi un costruttore alla classe Aquarium
che hai creato nella prima attività. Nell'esempio precedente, ogni istanza di Aquarium
viene creata con le stesse dimensioni. Puoi modificare le dimensioni una volta create impostando le proprietà, ma sarebbe più semplice creare le dimensioni corrette fin dall'inizio.
In alcuni linguaggi di programmazione, il costruttore viene definito creando un metodo all'interno della classe che ha lo stesso nome della classe. In Kotlin, definisci il costruttore direttamente nella dichiarazione della classe, specificando i parametri tra parentesi come se la classe fosse un metodo. Come per le funzioni in Kotlin, questi parametri possono includere valori predefiniti.
- Nella classe
Aquarium
che hai creato in precedenza, modifica la definizione della classe in modo da includere tre parametri del costruttore con valori predefiniti perlength
,width
eheight
e assegnali alle proprietà corrispondenti.
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
...
}
- Il modo più compatto per farlo in Kotlin è definire le proprietà direttamente con il costruttore, utilizzando
var
oval
. Kotlin crea automaticamente anche i metodi getter e setter. Dopodiché puoi rimuovere le definizioni delle proprietà nel corpo della classe.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
- Quando crei un oggetto
Aquarium
con questo costruttore, puoi specificare nessun argomento e ottenere i valori predefiniti, specificarne solo alcuni o specificarli tutti e creare un oggettoAquarium
di dimensioni completamente personalizzate. Nella funzionebuildAquarium()
, prova diversi modi per creare un oggettoAquarium
utilizzando i parametri denominati.
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()
}
- Esegui il programma e osserva l'output.
⇒ 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
Nota che non è stato necessario sovraccaricare il costruttore e scrivere una versione diversa per ciascuno di questi casi (più alcune altre per le altre combinazioni). Kotlin crea ciò che è necessario dai valori predefiniti e dai parametri denominati.
Passaggio 2: aggiungi i blocchi di inizializzazione
I costruttori di esempio precedenti dichiarano solo le proprietà e assegnano loro il valore di un'espressione. Se il costruttore ha bisogno di altro codice di inizializzazione, può essere inserito in uno o più blocchi init
. In questo passaggio, aggiungi alcuni blocchi init
alla classe Aquarium
.
- Nella classe
Aquarium
, aggiungi un bloccoinit
per stampare che l'oggetto è in fase di inizializzazione e un secondo blocco per stampare il volume in litri.
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")
}
}
- Esegui il programma e osserva l'output.
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
Tieni presente che i blocchi init
vengono eseguiti nell'ordine in cui appaiono nella definizione della classe e che vengono eseguiti tutti quando viene chiamato il costruttore.
Passaggio 3: scopri di più sui costruttori secondari
In questo passaggio, scoprirai di più sui costruttori secondari e ne aggiungerai uno alla tua classe. Oltre a un costruttore primario, che può avere uno o più blocchi init
, una classe Kotlin può avere anche uno o più costruttori secondari per consentire l'overload del costruttore, ovvero costruttori con argomenti diversi.
- Nella classe
Aquarium
, aggiungi un costruttore secondario che accetti un numero di pesci come argomento, utilizzando la parola chiaveconstructor
. Crea una proprietàval
per il volume calcolato dell'acquario in litri in base al numero di pesci. Considera 2 litri (2000 cm³) di acqua per pesce, più un po' di spazio extra per evitare che l'acqua fuoriesca.
constructor(numberOfFish: Int) : this() {
// 2,000 cm^3 per fish + extra room so water doesn't spill
val tank = numberOfFish * 2000 * 1.1
}
- All'interno del costruttore secondario, mantieni invariate la lunghezza e la larghezza (impostate nel costruttore principale) e calcola l'altezza necessaria per ottenere il volume specificato del serbatoio.
// calculate the height needed
height = (tank / (length * width)).toInt()
- Nella funzione
buildAquarium()
, aggiungi una chiamata per creare unAquarium
utilizzando il nuovo costruttore secondario. Stampa le dimensioni e il volume.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
- Esegui il programma e osserva l'output.
⇒ aquarium initializing Volume: 80 l Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Tieni presente che il volume viene stampato due volte: una volta dal blocco init
nel costruttore principale prima dell'esecuzione del costruttore secondario e una volta dal codice in buildAquarium()
.
Avresti potuto includere la parola chiave constructor
anche nel costruttore principale, ma nella maggior parte dei casi non è necessario.
Passaggio 4: aggiungi un nuovo getter di proprietà
In questo passaggio, aggiungi un getter di proprietà esplicito. Kotlin definisce automaticamente i metodi getter e setter quando definisci le proprietà, ma a volte il valore di una proprietà deve essere aggiustato o calcolato. Ad esempio, sopra hai stampato il volume di Aquarium
. Puoi rendere il volume disponibile come proprietà definendo una variabile e un getter per la proprietà. Poiché volume
deve essere calcolato, il getter deve restituire il valore calcolato, cosa che puoi fare con una funzione di una riga.
- Nella classe
Aquarium
, definisci una proprietàInt
chiamatavolume
e un metodoget()
che calcola il volume nella riga successiva.
val volume: Int
get() = width * height * length / 1000 // 1000 cm^3 = 1 l
- Rimuovi il blocco
init
che stampa il volume. - Rimuovi il codice in
buildAquarium()
che stampa il volume. - Nel metodo
printSize()
, aggiungi una riga per stampare il volume.
fun printSize() {
println("Width: $width cm " +
"Length: $length cm " +
"Height: $height cm "
)
// 1 l = 1000 cm^3
println("Volume: $volume l")
}
- Esegui il programma e osserva l'output.
⇒ aquarium initializing Width: 20 cm Length: 100 cm Height: 31 cm Volume: 62 l
Le dimensioni e il volume sono gli stessi di prima, ma il volume viene stampato una sola volta dopo che l'oggetto è stato inizializzato completamente sia dal costruttore principale che da quello secondario.
Passaggio 5: aggiungi un setter di proprietà
In questo passaggio, crei un nuovo setter di proprietà per il volume.
- Nella classe
Aquarium
, modificavolume
invar
in modo che possa essere impostato più di una volta. - Aggiungi un setter per la proprietà
volume
aggiungendo un metodoset()
sotto il getter, che ricalcola l'altezza in base alla quantità di acqua fornita. Per convenzione, il nome del parametro setter èvalue
, ma puoi modificarlo se preferisci.
var volume: Int
get() = width * height * length / 1000
set(value) {
height = (value * 1000) / (width * length)
}
- In
buildAquarium()
, aggiungi il codice per impostare il volume dell'acquario a 70 litri. Stampa la nuova dimensione.
fun buildAquarium() {
val aquarium6 = Aquarium(numberOfFish = 29)
aquarium6.printSize()
aquarium6.volume = 70
aquarium6.printSize()
}
- Esegui di nuovo il programma e osserva l'altezza e il volume modificati.
⇒ 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
Finora nel codice non sono stati utilizzati modificatori di visibilità, come public
o private
. Questo perché, per impostazione predefinita, tutto in Kotlin è pubblico, il che significa che è possibile accedere a tutto ovunque, incluse classi, metodi, proprietà e variabili membro.
In Kotlin, classi, oggetti, interfacce, costruttori, funzioni, proprietà e i relativi setter possono avere modificatori di visibilità:
public
significa visibile al di fuori del corso. Per impostazione predefinita, tutto è pubblico, incluse le variabili e i metodi della classe.internal
significa che sarà visibile solo all'interno di quel modulo. Un modulo è un insieme di file Kotlin compilati insieme, ad esempio una libreria o un'applicazione.private
significa che sarà visibile solo in quella classe (o nel file di origine se stai lavorando con le funzioni).protected
è uguale aprivate
, ma sarà visibile anche a tutte le sottoclassi.
Per saperne di più, consulta la sezione Modificatori di visibilità nella documentazione di Kotlin.
Variabili membro
Le proprietà all'interno di una classe o le variabili membro sono public
per impostazione predefinita. Se li definisci con var
, sono modificabili, ovvero leggibili e scrivibili. Se li definisci con val
, sono di sola lettura dopo l'inizializzazione.
Se vuoi una proprietà che il tuo codice possa leggere o scrivere, ma che il codice esterno possa solo leggere, puoi lasciare la proprietà e il relativo getter come pubblici e dichiarare il setter privato, come mostrato di seguito.
var volume: Int
get() = width * height * length / 1000
private set(value) {
height = (value * 1000) / (width * length)
}
In questa attività imparerai come funzionano le sottoclassi e l'ereditarietà in Kotlin. Sono simili a quelli che hai visto in altre lingue, ma ci sono alcune differenze.
In Kotlin, per impostazione predefinita, non è possibile creare sottoclassi delle classi. Allo stesso modo, le proprietà e le variabili membro non possono essere sostituite dalle sottoclassi (anche se è possibile accedervi).
Per consentire la creazione di sottoclassi, devi contrassegnare una classe come open
. Allo stesso modo, devi contrassegnare le proprietà e le variabili membro come open
per eseguirne l'override nella sottoclasse. La parola chiave open
è obbligatoria per evitare di divulgare accidentalmente i dettagli di implementazione come parte dell'interfaccia della classe.
Passaggio 1: apri il corso Acquario
In questo passaggio, rendi la classe Aquarium
open
, in modo da poterla sostituire nel passaggio successivo.
- Contrassegna la classe
Aquarium
e tutte le relative proprietà con la parola chiaveopen
.
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)
}
- Aggiungi una proprietà
shape
aperta con il valore"rectangle"
.
open val shape = "rectangle"
- Aggiungi una proprietà
water
aperta con un getter che restituisce il 90% del volume diAquarium
.
open var water: Double = 0.0
get() = volume * 0.9
- Aggiungi il codice al metodo
printSize()
per stampare la forma e la quantità di acqua come percentuale del 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)")
}
- In
buildAquarium()
, modifica il codice per creare unAquarium
conwidth = 25
,length = 25
eheight = 40
.
fun buildAquarium() {
val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
aquarium6.printSize()
}
- Esegui il programma e osserva il nuovo output.
⇒ aquarium initializing rectangle Width: 25 cm Length: 25 cm Height: 40 cm Volume: 25 l Water: 22.5 l (90.0% full)
Passaggio 2: crea una sottoclasse
- Crea una sottoclasse di
Aquarium
denominataTowerTank
, che implementa un serbatoio cilindrico arrotondato anziché un serbatoio rettangolare. Puoi aggiungereTowerTank
sottoAquarium
, perché puoi aggiungere un altro corso nello stesso file del corsoAquarium
. - In
TowerTank
, esegui l'override della proprietàheight
, definita nel costruttore. Per eseguire l'override di una proprietà, utilizza la parola chiaveoverride
nella sottoclasse.
- Fai in modo che il costruttore di
TowerTank
accetti undiameter
. Utilizzadiameter
sia perlength
sia perwidth
quando chiami il costruttore nella superclasseAquarium
.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
- Esegui l'override della proprietà del volume per calcolare un cilindro. La formula per un cilindro è pi greco per il raggio al quadrato per l'altezza. Devi importare la costante
PI
dajava.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()
}
- In
TowerTank
, sostituisci la proprietàwater
in modo che sia l'80% del volume.
override var water = volume * 0.8
- Esegui l'override di
shape
impostandolo su"cylinder"
.
override val shape = "cylinder"
- La classe
TowerTank
finale dovrebbe essere simile al codice riportato di seguito.
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"
}
- In
buildAquarium()
, crea unTowerTank
con un diametro di 25 cm e un'altezza di 45 cm. Stampa la taglia.
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()
}
- Esegui il programma e osserva l'output.
⇒ 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)
A volte vuoi definire un comportamento o proprietà comuni da condividere tra alcune classi correlate. Kotlin offre due modi per farlo: interfacce e classi astratte. In questa attività, crei una classe astratta AquariumFish
per le proprietà comuni a tutti i pesci. Crei un'interfaccia chiamata FishAction
per definire il comportamento comune a tutti i pesci.
- Né una classe astratta né un'interfaccia possono essere istanziate autonomamente, il che significa che non puoi creare oggetti di questi tipi direttamente.
- Le classi astratte hanno costruttori.
- Le interfacce non possono avere alcuna logica di costruttore o memorizzare alcuno stato.
Passaggio 1: Creare una classe astratta
- In example.myapp, crea un nuovo file,
AquariumFish.kt
. - Crea una classe, chiamata anche
AquariumFish
, e contrassegnala conabstract
. - Aggiungi una proprietà
String
,color
, e contrassegnala conabstract
.
package example.myapp
abstract class AquariumFish {
abstract val color: String
}
- Crea due sottoclassi di
AquariumFish
,Shark
ePlecostomus
. - Poiché
color
è astratta, le sottoclassi devono implementarla. RendiShark
grigio ePlecostomus
oro.
class Shark: AquariumFish() {
override val color = "gray"
}
class Plecostomus: AquariumFish() {
override val color = "gold"
}
- In main.kt, crea una funzione
makeFish()
per testare le tue classi. Crea unShark
e unPlecostomus
, poi stampa il colore di ciascuno. - Elimina il codice di test precedente in
main()
e aggiungi una chiamata amakeFish()
. Il codice dovrebbe essere simile a quello riportato di seguito.
main.kt
:
package example.myapp
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
println("Plecostomus: ${pleco.color}")
}
fun main () {
makeFish()
}
- Esegui il programma e osserva l'output.
⇒ Shark: gray Plecostomus: gold
Il seguente diagramma rappresenta la classe Shark
e la classe Plecostomus
, che sono sottoclassi della classe astratta AquariumFish
.
Passaggio 2: Crea un'interfaccia
- In AquariumFish.kt, crea un'interfaccia denominata
FishAction
con un metodoeat()
.
interface FishAction {
fun eat()
}
- Aggiungi
FishAction
a ciascuna delle sottoclassi e implementaeat()
facendo in modo che stampi ciò che fa il pesce.
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")
}
}
- Nella funzione
makeFish()
, fai mangiare qualcosa a ogni pesce che hai creato chiamandoeat()
.
fun makeFish() {
val shark = Shark()
val pleco = Plecostomus()
println("Shark: ${shark.color}")
shark.eat()
println("Plecostomus: ${pleco.color}")
pleco.eat()
}
- Esegui il programma e osserva l'output.
⇒ Shark: gray hunt and eat fish Plecostomus: gold eat algae
Il seguente diagramma rappresenta la classe Shark
e la classe Plecostomus
, entrambe composte da e implementano l'interfaccia FishAction
.
Quando utilizzare le classi astratte rispetto alle interfacce
Gli esempi riportati sopra sono semplici, ma quando hai molte classi correlate, le classi astratte e le interfacce possono aiutarti a mantenere il design più pulito, organizzato e facile da gestire.
Come indicato sopra, le classi astratte possono avere costruttori, mentre le interfacce no, ma per il resto sono molto simili. Quindi, quando devi utilizzare ciascuna?
Quando utilizzi le interfacce per comporre una classe, la funzionalità della classe viene estesa tramite le istanze di classe che contiene. La composizione tende a rendere il codice più facile da riutilizzare e da comprendere rispetto all'ereditarietà da una classe astratta. Inoltre, puoi utilizzare più interfacce in una classe, ma puoi creare sottoclassi solo da una classe astratta.
La composizione spesso porta a un'incapsulamento migliore, a un accoppiamento (interdipendenza) inferiore, a interfacce più pulite e a un codice più utilizzabile. Per questi motivi, l'utilizzo della composizione con le interfacce è la progettazione preferita. D'altra parte, l'ereditarietà da una classe astratta tende a essere adatta ad alcuni problemi. Pertanto, dovresti preferire la composizione, ma quando l'ereditarietà ha senso, Kotlin ti consente di farlo.
- Utilizza un'interfaccia se hai molti metodi e una o due implementazioni predefinite, ad esempio come in
AquariumAction
di seguito.
interface AquariumAction {
fun eat()
fun jump()
fun clean()
fun catchFish()
fun swim() {
println("swim")
}
}
- Utilizza una classe astratta ogni volta che non riesci a completare una classe. Ad esempio, tornando alla classe
AquariumFish
, puoi fare in modo che tutte le classiAquariumFish
implementinoFishAction
e fornire un'implementazione predefinita pereat
lasciandocolor
astratto, perché non esiste un colore predefinito per i pesci.
interface FishAction {
fun eat()
}
abstract class AquariumFish: FishAction {
abstract val color: String
override fun eat() = println("yum")
}
L'attività precedente ha introdotto le classi astratte, le interfacce e il concetto di composizione. La delega dell'interfaccia è una tecnica avanzata in cui i metodi di un'interfaccia vengono implementati da un oggetto helper (o delegato), che viene poi utilizzato da una classe. Questa tecnica può essere utile quando utilizzi un'interfaccia in una serie di classi non correlate: aggiungi la funzionalità dell'interfaccia necessaria a una classe helper separata e ciascuna delle classi utilizza un'istanza della classe helper per implementare la funzionalità.
In questa attività utilizzi la delega dell'interfaccia per aggiungere funzionalità a una classe.
Passaggio 1: crea una nuova interfaccia
- In AquariumFish.kt, rimuovi la classe
AquariumFish
. Anziché ereditare dalla classeAquariumFish
,Plecostomus
eShark
implementeranno interfacce sia per l'azione del pesce sia per il suo colore. - Crea una nuova interfaccia,
FishColor
, che definisce il colore come stringa.
interface FishColor {
val color: String
}
- Modifica
Plecostomus
per implementare due interfacce,FishAction
eFishColor
. Devi eseguire l'override dicolor
daFishColor
e dieat()
daFishAction
.
class Plecostomus: FishAction, FishColor {
override val color = "gold"
override fun eat() {
println("eat algae")
}
}
- Modifica la classe
Shark
in modo che implementi anche le due interfacce,FishAction
eFishColor
, anziché ereditare daAquariumFish
.
class Shark: FishAction, FishColor {
override val color = "gray"
override fun eat() {
println("hunt and eat fish")
}
}
- Il codice finale dovrebbe avere un aspetto simile al seguente:
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")
}
}
Passaggio 2: crea una classe singleton
Successivamente, implementa la configurazione per la parte di delega creando una classe helper che implementa FishColor
. Crei una classe di base chiamata GoldColor
che implementa FishColor
. L'unica cosa che fa è dire che il suo colore è oro.
Non ha senso creare più istanze di GoldColor
, perché farebbero esattamente la stessa cosa. Pertanto, Kotlin ti consente di dichiarare una classe in cui puoi creare una sola istanza utilizzando la parola chiave object
anziché class
. Kotlin creerà un'istanza e a questa istanza viene fatto riferimento dal nome della classe. Quindi tutti gli altri oggetti possono utilizzare questa singola istanza. Non è possibile creare altre istanze di questa classe. Se hai familiarità con il pattern singleton, ecco come implementare i singleton in Kotlin.
- In AquariumFish.kt, crea un oggetto per
GoldColor
. Esegui l'override del colore.
object GoldColor : FishColor {
override val color = "gold"
}
Passaggio 3: aggiungi la delega dell'interfaccia per FishColor
Ora puoi utilizzare la delega dell'interfaccia.
- In AquariumFish.kt, rimuovi l'override di
color
daPlecostomus
. - Modifica la classe
Plecostomus
per ottenere il colore daGoldColor
. Per farlo, aggiungiby GoldColor
alla dichiarazione della classe, creando la delega. Ciò significa che, anziché implementareFishColor
, utilizza l'implementazione fornita daGoldColor
. Pertanto, ogni volta che si accede acolor
, l'accesso viene delegato aGoldColor
.
class Plecostomus: FishAction, FishColor by GoldColor {
override fun eat() {
println("eat algae")
}
}
Con la classe così com'è, tutti i Pleco saranno dorati, ma in realtà questi pesci sono disponibili in molti colori. Puoi risolvere il problema aggiungendo un parametro del costruttore per il colore con GoldColor
come colore predefinito per Plecostomus
.
- Modifica la classe
Plecostomus
in modo che accetti un valore passato infishColor
con il relativo costruttore e imposta il valore predefinito suGoldColor
. Modifica la delega daby GoldColor
aby fishColor
.
class Plecostomus(fishColor: FishColor = GoldColor): FishAction,
FishColor by fishColor {
override fun eat() {
println("eat algae")
}
}
Passaggio 4: aggiungi la delega dell'interfaccia per FishAction
Allo stesso modo, puoi utilizzare la delega dell'interfaccia per FishAction
.
- In AquariumFish.kt crea una classe
PrintingFishAction
che implementaFishAction
, che accettaString
,food
, quindi stampa ciò che mangia il pesce.
class PrintingFishAction(val food: String) : FishAction {
override fun eat() {
println(food)
}
}
- Nella classe
Plecostomus
, rimuovi la funzione di overrideeat()
, perché la sostituirai con una delega. - Nella dichiarazione di
Plecostomus
, delegaFishAction
aPrintingFishAction
, passando"eat algae"
. - Con tutta questa delega, non c'è codice nel corpo della classe
Plecostomus
, quindi rimuovi{}
, perché tutti gli override vengono gestiti dalla delega dell'interfaccia
class Plecostomus (fishColor: FishColor = GoldColor):
FishAction by PrintingFishAction("eat algae"),
FishColor by fishColor
Il seguente diagramma rappresenta le classi Shark
e Plecostomus
, entrambe composte dalle interfacce PrintingFishAction
e FishColor
, ma che delegano l'implementazione a queste ultime.
La delega dell'interfaccia è potente e in genere dovresti considerare come utilizzarla ogni volta che potresti utilizzare una classe astratta in un altro linguaggio. Ti consente di utilizzare la composizione per inserire comportamenti, anziché richiedere molte sottoclassi, ognuna specializzata in un modo diverso.
Una classe di dati è simile a una struct
in altri linguaggi: esiste principalmente per contenere alcuni dati, ma un oggetto classe di dati è comunque un oggetto. Gli oggetti della classe di dati Kotlin hanno alcuni vantaggi aggiuntivi, come le utilità per la stampa e la copia. In questa attività, creerai una semplice classe di dati e scoprirai il supporto fornito da Kotlin per le classi di dati.
Passaggio 1: crea una classe di dati
- Aggiungi un nuovo pacchetto
decor
al pacchetto example.myapp per contenere il nuovo codice. Fai clic con il tasto destro del mouse su example.myapp nel riquadro Progetto e seleziona File > Nuovo > Pacchetto. - Nel pacchetto, crea una nuova classe chiamata
Decoration
.
package example.myapp.decor
class Decoration {
}
- Per trasformare
Decoration
in una classe di dati, aggiungi il prefissodata
alla dichiarazione della classe. - Aggiungi una proprietà
String
denominatarocks
per fornire alcuni dati alla classe.
data class Decoration(val rocks: String) {
}
- Nel file, al di fuori della classe, aggiungi una funzione
makeDecorations()
per creare e stampare un'istanza di unDecoration
con"granite"
.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
}
- Aggiungi una funzione
main()
per chiamaremakeDecorations()
ed esegui il programma. Nota l'output sensato creato perché si tratta di una classe di dati.
⇒ Decoration(rocks=granite)
- In
makeDecorations()
, crea altre due istanze dell'oggettoDecoration
che siano entrambe "slate" e stampale.
fun makeDecorations() {
val decoration1 = Decoration("granite")
println(decoration1)
val decoration2 = Decoration("slate")
println(decoration2)
val decoration3 = Decoration("slate")
println(decoration3)
}
- In
makeDecorations()
, aggiungi un'istruzione di stampa che stampi il risultato del confronto tradecoration1
edecoration2
e una seconda che confrontidecoration3
edecoration2
. Utilizza il metodo equals() fornito dalle classi di dati.
println (decoration1.equals(decoration2))
println (decoration3.equals(decoration2))
- Esegui il codice.
⇒ Decoration(rocks=granite) Decoration(rocks=slate) Decoration(rocks=slate) false true
Passaggio 2: Utilizzare la destrutturazione
Per accedere alle proprietà di un oggetto dati e assegnarle alle variabili, puoi assegnarle una alla volta, in questo modo.
val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver
Puoi invece creare variabili, una per ogni proprietà, e assegnare l'oggetto dati al gruppo di variabili. Kotlin inserisce il valore della proprietà in ogni variabile.
val (rock, wood, diver) = decoration
Questa operazione è chiamata destrutturazione ed è una scorciatoia utile. Il numero di variabili deve corrispondere al numero di proprietà e le variabili vengono assegnate nell'ordine in cui vengono dichiarate nella classe. Ecco un esempio completo che puoi provare in 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 non hai bisogno di una o più proprietà, puoi saltarle utilizzando _
anziché un nome di variabile, come mostrato nel codice riportato di seguito.
val (rock, _, diver) = d5
In questa attività, imparerai a conoscere alcune delle classi speciali in Kotlin, tra cui:
- Lezioni di singleton
- Enum
- Classi sigillate
Passaggio 1: richiama le classi singleton
Ricorda l'esempio precedente con la classe GoldColor
.
object GoldColor : FishColor {
override val color = "gold"
}
Poiché ogni istanza di GoldColor
esegue la stessa operazione, viene dichiarata come object
anziché come class
per renderla un singleton. Può esserci una sola istanza.
Passaggio 2: crea un'enumerazione
Kotlin supporta anche le enumerazioni, che ti consentono di enumerare qualcosa e farvi riferimento per nome, proprio come in altri linguaggi. Dichiara un'enumerazione anteponendo alla dichiarazione la parola chiave enum
. Una dichiarazione enum di base richiede solo un elenco di nomi, ma puoi anche definire uno o più campi associati a ogni nome.
- In Decoration.kt, prova un esempio di enumerazione.
enum class Color(val rgb: Int) {
RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}
Gli enum sono un po' come i singleton: può essercene solo uno e solo uno di ogni valore nell'enumerazione. Ad esempio, può esserci un solo Color.RED
, un solo Color.GREEN
e un solo Color.BLUE
. In questo esempio, i valori RGB vengono assegnati alla proprietà rgb
per rappresentare i componenti del colore. Puoi anche ottenere il valore ordinale di un'enumerazione utilizzando la proprietà ordinal
e il relativo nome utilizzando la proprietà name
.
- Prova un altro esempio di enumerazione.
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
Passaggio 3: crea una classe sigillata
Una classe sigillata è una classe di cui è possibile creare sottoclassi, ma solo all'interno del file in cui è dichiarata. Se provi a creare una sottoclasse della classe in un file diverso, ricevi un errore.
Poiché le classi e le sottoclassi si trovano nello stesso file, Kotlin conoscerà staticamente tutte le sottoclassi. ovvero, in fase di compilazione, il compilatore vede tutte le classi e le sottoclassi e sa che sono tutte, quindi può eseguire controlli aggiuntivi per te.
- In AquariumFish.kt, prova un esempio di classe sigillata, mantenendo il tema acquatico.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()
fun matchSeal(seal: Seal): String {
return when(seal) {
is Walrus -> "walrus"
is SeaLion -> "sea lion"
}
}
La classe Seal
non può essere sottoclassata in un altro file. Se vuoi aggiungere altri tipi di Seal
, devi farlo nello stesso file. Ciò rende le classi sigillate un modo sicuro per rappresentare un numero fisso di tipi. Ad esempio, le classi sigillate sono ideali per restituire esito positivo o errore da un'API di rete.
In questa lezione abbiamo trattato molti argomenti. Sebbene gran parte di questo linguaggio sia simile ad altri linguaggi di programmazione orientati agli oggetti, Kotlin aggiunge alcune funzionalità per mantenere il codice conciso e leggibile.
Classi e costruttori
- Definisci una classe in Kotlin utilizzando
class
. - Kotlin crea automaticamente setter e getter per le proprietà.
- Definisci il costruttore primario direttamente nella definizione della classe. Ad esempio:
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
- Se un costruttore primario richiede codice aggiuntivo, scrivilo in uno o più blocchi
init
. - Una classe può definire uno o più costruttori secondari utilizzando
constructor
, ma lo stile Kotlin prevede l'utilizzo di una funzione di fabbrica.
Modificatori di visibilità e sottoclassi
- Tutte le classi e le funzioni in Kotlin sono
public
per impostazione predefinita, ma puoi utilizzare i modificatori per modificare la visibilità ininternal
,private
oprotected
. - Per creare una sottoclasse, la classe principale deve essere contrassegnata con
open
. - Per eseguire l'override di metodi e proprietà in una sottoclasse, i metodi e le proprietà devono essere contrassegnati con
open
nella classe padre. - Una classe sigillata può essere sottoclassata solo nello stesso file in cui è definita. Crea una classe sigillata anteponendo alla dichiarazione
sealed
.
Classi di dati, singleton ed enum
- Crea una classe di dati anteponendo alla dichiarazione
data
. - La destrutturazione è una notazione abbreviata per assegnare le proprietà di un oggetto
data
a variabili separate. - Crea una classe singleton utilizzando
object
anzichéclass
. - Definisci un enum utilizzando
enum class
.
Classi astratte, interfacce e delega
- Le classi astratte e le interfacce sono due modi per condividere un comportamento comune tra le classi.
- Una classe astratta definisce proprietà e comportamento, ma lascia l'implementazione alle sottoclassi.
- Un'interfaccia definisce il comportamento e può fornire implementazioni predefinite per parte o tutto il comportamento.
- Quando utilizzi le interfacce per comporre una classe, la funzionalità della classe viene estesa tramite le istanze di classe che contiene.
- La delega di interfaccia utilizza la composizione, ma delega anche l'implementazione alle classi di interfaccia.
- La composizione è un modo efficace per aggiungere funzionalità a una classe utilizzando la delega dell'interfaccia. In generale, la composizione è preferita, ma l'ereditarietà da una classe astratta è più adatta ad alcuni problemi.
Documentazione di Kotlin
Se vuoi maggiori informazioni su un argomento di questo corso o se hai difficoltà, https://kotlinlang.org è il punto di partenza migliore.
- Classi ed ereditarietà
- Costruttori
- Funzioni di fabbrica
- Proprietà e campi
- Modificatori di visibilità
- Classi astratte
- Interfacce
- Delega
- Classi di dati
- Equality
- Destrutturazione
- Dichiarazioni degli oggetti
- Classi enum
- Classi sigillate
- Gestione degli errori facoltativi utilizzando le classi sigillate di Kotlin
Tutorial di Kotlin
Il sito web https://try.kotlinlang.org include tutorial dettagliati chiamati Kotlin Koans, un interprete basato sul web e una serie completa di documentazione di riferimento con esempi.
Corso Udacity
Per visualizzare il corso Udacity su questo argomento, consulta Kotlin Bootcamp for Programmers.
IntelliJ IDEA
La documentazione di IntelliJ IDEA è disponibile sul sito web di JetBrains.
Questa sezione elenca i possibili compiti a casa per gli studenti che seguono questo codelab nell'ambito di un corso guidato da un insegnante. Spetta all'insegnante:
- Assegna i compiti, se richiesto.
- Comunica agli studenti come inviare i compiti.
- Valuta i compiti a casa.
Gli insegnanti possono utilizzare questi suggerimenti nella misura che ritengono opportuna e sono liberi di assegnare qualsiasi altro compito a casa che ritengono appropriato.
Se stai seguendo questo codelab in autonomia, sentiti libero di utilizzare questi compiti per casa per mettere alla prova le tue conoscenze.
Rispondi a queste domande
Domanda 1
Le classi hanno un metodo speciale che funge da modello per la creazione di oggetti di quella classe. Come si chiama il metodo?
▢ Un costruttore
▢ Un instanziatore
▢ Un costruttore
▢ Un progetto
Domanda 2
Quale delle seguenti affermazioni su interfacce e classi astratte NON è corretta?
▢ Le classi astratte possono avere costruttori.
▢ Le interfacce non possono avere costruttori.
▢ Le interfacce e le classi astratte possono essere istanziate direttamente.
▢ Le proprietà astratte devono essere implementate dalle sottoclassi della classe astratta.
Domanda 3
Quale dei seguenti NON è un modificatore di visibilità Kotlin per proprietà, metodi e così via?
▢ internal
▢ nosubclass
▢ protected
▢ private
Domanda 4
Considera questa classe di dati:data class Fish(val name: String, val species:String, val colors:String)
Quale dei seguenti codici NON è valido per creare e destrutturare un oggetto 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")
Domanda 5
Supponiamo che tu abbia uno zoo con molti animali che hanno bisogno di cure. Quale delle seguenti azioni NON farebbe parte dell'implementazione della gestione?
▢ Un interface
per i diversi tipi di alimenti che mangiano gli animali.
▢ Una classe abstract Caretaker
da cui puoi creare diversi tipi di tutori.
▢ Un interface
per aver dato acqua pulita a un animale.
▢ Un corso data
per una voce in un programma di alimentazione.
Passa alla lezione successiva:
Per una panoramica del corso, inclusi i link ad altri codelab, vedi "Kotlin Bootcamp for Programmers: Welcome to the course" (Kotlin Bootcamp per programmatori: benvenuto al corso).