Kotlin Bootcamp for Programmers 4: Object-oriented programming

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 oggetto Aquarium.
  • 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 metodo clean(). La classe Aquarium potrebbe implementare l'interfaccia Clean 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.

  1. Nel riquadro Progetto, sotto il progetto Hello Kotlin, fai clic con il tasto destro del mouse sulla cartella src.
  2. 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.

  1. Fai clic con il tasto destro del mouse sul pacchetto example.myapp.
  2. Seleziona Nuovo > File / classe Kotlin.
  3. In Tipo, seleziona Corso e assegna al corso il nome Aquarium. IntelliJ IDEA include il nome del pacchetto nel file e crea una classe Aquarium vuota.
  4. 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().

  1. Nel riquadro Progetto a sinistra, fai clic con il tasto destro del mouse sul pacchetto example.myapp.
  2. Seleziona Nuovo > File / classe Kotlin.
  3. 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.
  4. Definisci una funzione buildAquarium() e al suo interno crea un'istanza di Aquarium. 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 classe Aquarium, in modo simile all'utilizzo di new in altri linguaggi.
  5. Definisci una funzione main() e chiama buildAquarium().
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

Passaggio 4: aggiungi un metodo

  1. 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 ")
    }
  1. In main.kt, in buildAquarium(), chiama il metodo printSize() su myAquarium.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. Esegui il programma facendo clic sul triangolo verde accanto alla funzione main(). Osserva il risultato.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. 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()
}
  1. 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.

  1. Nella classe Aquarium che hai creato in precedenza, modifica la definizione della classe in modo da includere tre parametri del costruttore con valori predefiniti per length, width e height 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
...
}
  1. Il modo più compatto per farlo in Kotlin è definire le proprietà direttamente con il costruttore, utilizzando var o val. 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) {
...
}
  1. 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 oggetto Aquarium di dimensioni completamente personalizzate. Nella funzione buildAquarium(), prova diversi modi per creare un oggetto Aquarium 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()
}
  1. 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.

  1. Nella classe Aquarium, aggiungi un blocco init 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")
    }
}
  1. 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.

  1. Nella classe Aquarium, aggiungi un costruttore secondario che accetti un numero di pesci come argomento, utilizzando la parola chiave constructor. 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
}
  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()
  1. Nella funzione buildAquarium(), aggiungi una chiamata per creare un Aquarium 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")
}
  1. 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.

  1. Nella classe Aquarium, definisci una proprietà Int chiamata volume e un metodo get() che calcola il volume nella riga successiva.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. Rimuovi il blocco init che stampa il volume.
  2. Rimuovi il codice in buildAquarium() che stampa il volume.
  3. 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")
}
  1. 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.

  1. Nella classe Aquarium, modifica volume in var in modo che possa essere impostato più di una volta.
  2. Aggiungi un setter per la proprietà volume aggiungendo un metodo set() 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)
    }
  1. 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()
}
  1. 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 a private, 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.

  1. Contrassegna la classe Aquarium e tutte le relative proprietà con la parola chiave open.
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)
        }
  1. Aggiungi una proprietà shape aperta con il valore "rectangle".
   open val shape = "rectangle"
  1. Aggiungi una proprietà water aperta con un getter che restituisce il 90% del volume di Aquarium.
    open var water: Double = 0.0
        get() = volume * 0.9
  1. 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)")
}
  1. In buildAquarium(), modifica il codice per creare un Aquarium con width = 25, length = 25 e height = 40.
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. 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

  1. Crea una sottoclasse di Aquarium denominata TowerTank, che implementa un serbatoio cilindrico arrotondato anziché un serbatoio rettangolare. Puoi aggiungere TowerTank sotto Aquarium, perché puoi aggiungere un altro corso nello stesso file del corso Aquarium.
  2. In TowerTank, esegui l'override della proprietà height, definita nel costruttore. Per eseguire l'override di una proprietà, utilizza la parola chiave override nella sottoclasse.
  1. Fai in modo che il costruttore di TowerTank accetti un diameter. Utilizza diameter sia per length sia per width quando chiami il costruttore nella superclasse Aquarium.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. 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 da java.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()
    }
  1. In TowerTank, sostituisci la proprietà water in modo che sia l'80% del volume.
override var water = volume * 0.8
  1. Esegui l'override di shape impostandolo su "cylinder".
override val shape = "cylinder"
  1. 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"
}
  1. In buildAquarium(), crea un TowerTank 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()
}
  1. 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

  1. In example.myapp, crea un nuovo file, AquariumFish.kt.
  2. Crea una classe, chiamata anche AquariumFish, e contrassegnala con abstract.
  3. Aggiungi una proprietà String, color, e contrassegnala con abstract.
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. Crea due sottoclassi di AquariumFish, Shark e Plecostomus.
  2. Poiché color è astratta, le sottoclassi devono implementarla. Rendi Shark grigio e Plecostomus oro.
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. In main.kt, crea una funzione makeFish() per testare le tue classi. Crea un Shark e un Plecostomus, poi stampa il colore di ciascuno.
  2. Elimina il codice di test precedente in main() e aggiungi una chiamata a makeFish(). 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()
}
  1. 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.

Un diagramma che mostra la classe astratta AquariumFish e due sottoclassi, Shark e Plecostomus.

Passaggio 2: Crea un'interfaccia

  1. In AquariumFish.kt, crea un'interfaccia denominata FishAction con un metodo eat().
interface FishAction  {
    fun eat()
}
  1. Aggiungi FishAction a ciascuna delle sottoclassi e implementa eat() 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")
    }
}
  1. Nella funzione makeFish(), fai mangiare qualcosa a ogni pesce che hai creato chiamando eat().
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. 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 classi AquariumFish implementino FishAction e fornire un'implementazione predefinita per eat lasciando color 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

  1. In AquariumFish.kt, rimuovi la classe AquariumFish. Anziché ereditare dalla classe AquariumFish, Plecostomus e Shark implementeranno interfacce sia per l'azione del pesce sia per il suo colore.
  2. Crea una nuova interfaccia, FishColor, che definisce il colore come stringa.
interface FishColor {
    val color: String
}
  1. Modifica Plecostomus per implementare due interfacce, FishAction e FishColor. Devi eseguire l'override di color da FishColor e di eat() da FishAction.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Modifica la classe Shark in modo che implementi anche le due interfacce, FishAction e FishColor, anziché ereditare da AquariumFish.
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. 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.

  1. 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.

  1. In AquariumFish.kt, rimuovi l'override di color da Plecostomus.
  2. Modifica la classe Plecostomus per ottenere il colore da GoldColor. Per farlo, aggiungi by GoldColor alla dichiarazione della classe, creando la delega. Ciò significa che, anziché implementare FishColor, utilizza l'implementazione fornita da GoldColor. Pertanto, ogni volta che si accede a color, l'accesso viene delegato a GoldColor.
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.

  1. Modifica la classe Plecostomus in modo che accetti un valore passato in fishColor con il relativo costruttore e imposta il valore predefinito su GoldColor. Modifica la delega da by GoldColor a by 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.

  1. In AquariumFish.kt crea una classe PrintingFishAction che implementa FishAction, che accetta String, food, quindi stampa ciò che mangia il pesce.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. Nella classe Plecostomus, rimuovi la funzione di override eat(), perché la sostituirai con una delega.
  2. Nella dichiarazione di Plecostomus, delega FishAction a PrintingFishAction, passando "eat algae".
  3. 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

  1. 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.
  2. Nel pacchetto, crea una nuova classe chiamata Decoration.
package example.myapp.decor

class Decoration {
}
  1. Per trasformare Decoration in una classe di dati, aggiungi il prefisso data alla dichiarazione della classe.
  2. Aggiungi una proprietà String denominata rocks per fornire alcuni dati alla classe.
data class Decoration(val rocks: String) {
}
  1. Nel file, al di fuori della classe, aggiungi una funzione makeDecorations() per creare e stampare un'istanza di un Decoration con "granite".
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. Aggiungi una funzione main() per chiamare makeDecorations() ed esegui il programma. Nota l'output sensato creato perché si tratta di una classe di dati.
⇒ Decoration(rocks=granite)
  1. In makeDecorations(), crea altre due istanze dell'oggetto Decoration 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)
}
  1. In makeDecorations(), aggiungi un'istruzione di stampa che stampi il risultato del confronto tra decoration1 e decoration2 e una seconda che confronti decoration3 e decoration2. Utilizza il metodo equals() fornito dalle classi di dati.
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. 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.

  1. 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.

  1. 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.

  1. 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à in internal, private o protected.
  • 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.

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: 5.1 Estensioni

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).