Bottcamp Kotlin per programmatori 4: programmazione orientata agli oggetti

Questo codelab fa parte del Kotlin Bootcamp for Programs. Otterrai il massimo valore da questo corso se lavori in sequenza nei codelab. A seconda delle tue conoscenze, potresti essere in grado di scorrere alcune sezioni. Questo corso è rivolto ai programmatori che conoscono una lingua orientata agli oggetti e vogliono imparare il kotlin.

Introduzione

In questo codelab, creerai un programma Kotlin e scoprirai lezioni e oggetti in Kotlin. Gran parte di questi contenuti ti sarà familiare se conosci un'altra lingua orientata agli oggetti, ma Kotlin presenta alcune differenze importanti per ridurre la quantità di codice da scrivere. Scoprirai anche di più sulle classi astratte e sulla delega dell'interfaccia.

Anziché creare un'unica app di esempio, le lezioni di questo corso sono state concepite per sviluppare le tue conoscenze, ma sono semi-indipendenti gli uni dagli altri per esplorare le sezioni che conosci. Per collegare tutti questi esempi, molti sono basati su un acquario. E se vuoi vedere l'intera storia dell'acquario, dai un'occhiata al Bootcamp Kotlin per programmatoriCorso Udacity.

Informazioni importanti

  • Nozioni di base su Kotlin, tra cui tipi, operatori e looping
  • Sintassi delle funzioni di Kotlin
  • Nozioni di base della programmazione orientata agli oggetti
  • Nozioni di base su un IDE, come IntelliJ IDEA o Android Studio

Obiettivi didattici

  • Come creare corsi e accedere alle proprietà in Kotlin
  • Come creare e utilizzare costruttori di classe in Kotlin
  • Come creare una sottoclasse e come funziona l'ereditarietà
  • Informazioni su classi, interfacce e delega di interfaccia astratte
  • Come creare e utilizzare le classi di dati
  • Come utilizzare singleton, enumerazioni e classi sigillate

In questo lab proverai a:

  • Creare una classe con proprietà
  • Creare un costruttore per un corso
  • Crea una classe secondaria
  • Esamina esempi di interfacce e classi astratte
  • Creare una semplice classe di dati
  • Scopri di più su singleton, enumerazioni e classi chiuse

Dovresti già conoscere i seguenti termini di programmazione:

  • Le classi sono progetti per gli oggetti. Ad esempio, una classe Aquarium è il progetto base per creare un oggetto acquario.
  • Gli oggetti sono istanze di classi; un oggetto acquario è una Aquarium effettiva.
  • Le proprietà sono caratteristiche delle classi, come la lunghezza, la larghezza e l'altezza di un Aquarium.
  • I metodi, chiamati anche funzionalità membri, sono la funzionalità del corso. I metodi sono ciò che puoi fare con l'oggetto, Ad esempio, puoi fillWithWater() un oggetto Aquarium.
  • Un'interfaccia è una specifica che può essere implementata da una classe. Ad esempio, la pulizia è comune a oggetti diversi dagli acquari, e in genere la pulizia avviene in modi simili a seconda dei diversi oggetti. 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 importare i contenuti del pacchetto in un altro file e riutilizzare il codice e le classi al suo interno.

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 organizzare il codice.

  1. Nel riquadro Progetto, sotto il progetto Hello Kotlin, fai clic con il pulsante destro del mouse sulla cartella src.
  2. Seleziona Nuovo > pacchetto e chiamalo example.myapp.

Passaggio 2: crea un corso con le proprietà

I corsi sono definiti con la parola chiave class e i nomi delle classi iniziano per convenzione con una lettera maiuscola.

  1. Fai clic con il pulsante destro del mouse sul pacchetto example.myapp.
  2. Seleziona New > Kotlin File / Class (Nuovo > file/classe Kotlin).
  3. In Tipo, seleziona Classe e denomina il corso Aquarium. IntelliJ IDEA include il nome del pacchetto nel file e crea automaticamente 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 valori predefiniti.
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

Funzioni avanzate: Kotlin crea automaticamente getter e setter per le proprietà che hai definito nella classe Aquarium, consentendoti di 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 di Progetto a sinistra, fai clic con il pulsante destro del mouse sul pacchetto example.myapp.
  2. Seleziona New > Kotlin File / Class (Nuovo > file/classe Kotlin).
  3. Nel menu a discesa Tipo, mantieni la selezione come File e denomina il file main.kt. IntelliJ IDEA include il nome del pacchetto, ma non include una definizione della classe per il file.
  4. Definisci una funzione buildAquarium() e, all'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, come se utilizzassi new in altre lingue.
  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. Nel corso 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, a 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 il corso e continuerai a lavorare con le proprietà.

Passaggio 1: crea un costruttore

In questo passaggio devi aggiungere un costruttore alla classe Aquarium che hai creato nella prima attività. Nell'esempio precedente, ogni istanza di Aquarium viene creata con le stesse dimensioni. Una volta creata le proprietà, puoi modificarne le dimensioni impostandone le proprietà, ma sarebbe più semplice crearla con le dimensioni corrette.

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 stessa, specificando i parametri tra parentesi come se la classe fosse un metodo. Come nel caso delle funzioni in Kotlin, questi parametri possono includere valori predefiniti.

  1. Nella classe Aquarium creata in precedenza, modifica la definizione della classe per includere tre parametri 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 Kotlin è definire le proprietà direttamente con il costruttore, utilizzando var o val; inoltre, Kotlin crea automaticamente anche i getter e i setter. Successivamente, puoi rimuovere le definizioni delle proprietà dal corpo del corso.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. Quando crei un oggetto Aquarium con quel costruttore, non puoi specificare argomenti e ottenere i valori predefiniti, o specificarne solo alcuni, oppure specificarli tutti e creare un Aquarium completamente personalizzato. 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 

Tieni presente che non devi sovraccaricare il costruttore e scrivere una versione diversa per ognuno di questi casi (e alcuni altri per le altre combinazioni). Kotlin crea i dati necessari dai valori predefiniti e dai parametri denominati.

Passaggio 2: aggiungi i blocchi di inizializzazione

I costruttori di esempio riportati sopra si limitano a dichiarare proprietà e assegnare loro il valore di un'espressione. Se il costruttore ha bisogno di più codice di inizializzazione, può essere inserito in uno o più blocchi init. In questo passaggio devi aggiungere alcuni blocchi init alla classe Aquarium.

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

Nota che i blocchi init vengono eseguiti nell'ordine in cui sono visualizzati nella definizione della classe e vengono tutti eseguiti quando viene chiamato il costruttore.

Passaggio 3: scopri i costruttori secondari

In questo passaggio, imparerai i costruttori secondari e ne aggiungerai uno alla tua classe. Oltre a un costruttore principale, che può avere uno o più blocchi init, una classe Kotlin può avere anche uno o più costruttori secondari che consentono il sovraccarico dei costruttori, ovvero costruttori con argomenti diversi.

  1. Nella classe Aquarium, aggiungi un costruttore secondario che consideri diversi pesci come argomento, utilizzando la parola chiave constructor. Crea una proprietà della vasca val per il volume calcolato dell'acquario in litri, basato sul numero di pesci. Assumi 2 litri (2.000 cm^3) di acqua per pesce, più un po' di spazio in più per evitare l'acqua.
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 la stessa lunghezza e larghezza (impostate nel costruttore principale) e calcola l'altezza necessaria per rendere il serbatoio il volume sopra indicato.
    // 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

Nota che il volume viene stampato due volte, una dal blocco init nel costruttore principale prima che venga eseguito il costruttore secondario e una volta dal codice in buildAquarium().

Potresti aver incluso anche la parola chiave constructor nel costruttore principale, ma nella maggior parte dei casi non è necessaria.

Passaggio 4: aggiungi un nuovo getter proprietà

In questo passaggio devi aggiungere un getter di proprietà esplicito. Kotlin definisce automaticamente getter e setter quando definisci le proprietà, ma a volte il valore di una proprietà deve essere adeguato o calcolato. Ad esempio, in alto hai stampato il volume di Aquarium. Puoi rendere il volume disponibile come proprietà definendo una variabile e un getter. Poiché volume deve essere calcolato, il getter deve restituire il valore calcolato, che puoi fare con una funzione di una riga.

  1. Nella classe Aquarium, definisci una proprietà Int denominata volume e definisci un metodo get() che calcoli 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 le stesse di prima, ma il volume viene stampato una sola volta dopo che l'oggetto è stato completamente inizializzato dal costruttore principale e dal costruttore secondario.

Passaggio 5: aggiungi un setter proprietà

In questo passaggio, creerai un nuovo setter proprietà per il volume.

  1. Nel corso Aquarium, imposta volume su 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à d'acqua fornita. Per convenzione, il nome del parametro setter è value, ma se preferisci puoi modificarlo.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. In buildAquarium(), aggiungi un 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 non sono stati modificati modificatori, quali public o private. Questo perché, per impostazione predefinita, tutto in Kotlin è pubblico, il che significa che è possibile accedere a tutti ovunque, compresi corsi, metodi, proprietà e variabili relative ai membri.

In Kotlin, le classi, gli oggetti, le interfacce, i costruttori, le funzioni, le proprietà e i loro setter possono avere modificatori di visibilità:

  • public indica che non è visibile nella classe. Per impostazione predefinita, tutto è pubblico, incluse variabili e metodi del corso.
  • internal significa che sarà visibile solo all'interno di quel modulo. Un modulo è un insieme di file Kotlin raccolti insieme, ad esempio una biblioteca o un'applicazione.
  • private significa che sarà visibile solo in quella classe (o nel file di origine, se lavori con le funzioni).
  • protected è uguale a private, ma sarà visibile anche nelle eventuali classi secondarie.

Per ulteriori informazioni, consulta la sezione Modificatori di visibilità nella documentazione di Kotlin.

Variabili membri

Le proprietà all'interno di una classe o di variabili membri sono public per impostazione predefinita. Se li definisci con var, sono modificabili, ovvero leggibili e scrivibili. Se li definisci con val, saranno di sola lettura dopo l'inizializzazione.

Se vuoi una proprietà che il tuo codice possa leggere o scrivere, ma all'esterno il codice può solo leggere, puoi lasciare privati la proprietà e il getter 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 quanto hai visto in altre lingue, ma esistono alcune differenze.

In Kotlin, per impostazione predefinita, i corsi non possono essere suddivisi in sottoclassi. Analogamente, le proprietà e le variabili dei membri non possono essere sostituite da sottoclassi (anche se è possibile accedervi).

Devi contrassegnare un corso come open per consentire che venga classificato come secondario. Analogamente, devi contrassegnare le proprietà e le variabili membro come open per poterle sostituire nella sottoclasse. La parola chiave open è obbligatoria per evitare fughe di informazioni dettagliate sull'implementazione nell'interfaccia della classe.

Passaggio 1: apri la lezione sull'acquario

In questo passaggio, devi creare la classe Aquarium open, in modo da poterla sostituire nel passaggio successivo.

  1. Contrassegna la classe Aquarium e tutte le sue 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 chiamata TowerTank, che implementa un serbatoio a cilindro arrotondato anziché un serbatoio rettangolare. Puoi aggiungere TowerTank sotto Aquarium, poiché puoi aggiungere un altro corso nello stesso file della classe Aquarium.
  2. In TowerTank, sostituisci la proprietà height, definita nel costruttore. Per sostituire una proprietà, utilizza la parola chiave override nella sottoclasse.
  1. Crea il costruttore per TowerTank utilizzando un diameter. Usa diameter per length e 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 all'80% del volume.
override var water = volume * 0.8
  1. Sostituisci shape in modo che sia "cylinder".
override val shape = "cylinder"
  1. Il corso finale di TowerTank dovrebbe essere simile al seguente codice.

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 le dimensioni.

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 proprietà o comportamento comuni da condividere tra le classi correlate. Kotlin offre due modi per farlo: le interfacce e le classi astratte. In questa attività creerai una classe AquariumFish astratta per le proprietà comuni a tutti i pesci. Crei un'interfaccia chiamata FishAction per definire il comportamento comune a tutti i pesci.

  • Non è possibile creare autonomamente un'istanza di una classe astratta o di un'interfaccia, il che significa che non puoi creare oggetti di quel tipo direttamente.
  • Le classi astratte hanno costruttori.
  • Le interfacce non possono avere logiche di costruzione o archiviare stato.

Passaggio 1. Creare una classe astratta

  1. In example.myapp, crea un nuovo file, AquariumFish.kt.
  2. Crea un corso, chiamato anche AquariumFish, e contrassegnalo 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. color è astratto, per cui le sottoclassi devono implementarlo. Fai diventare 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 i tuoi corsi. Crea un'istanza di Shark e Plecostomus, quindi stampa il colore di ciascuno.
  2. Elimina il tuo codice di test precedente in main() e aggiungi una chiamata a makeFish(). Il tuo codice sarà simile al seguente:

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 sottoclasse la classe astratta, AquariumFish.

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

Passaggio 2. Creare un'interfaccia

  1. In AquariumFish.kt, crea un'interfaccia chiamata FishAction con un metodo eat().
interface FishAction  {
    fun eat()
}
  1. Aggiungi FishAction a ciascuna delle sottoclassi e implementa eat() chiedendogli di stampare le operazioni eseguite dai pesci.
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(), chiedi a ogni pesce che hai creato di mangiare qualcosa 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 e implementate nell'interfaccia di FishAction.

Quando utilizzare le classi astratte e le interfacce

Gli esempi riportati sopra sono semplici, ma se hai molte classi interconnesse, classi astratte e interfacce possono aiutarti a rendere il tuo design più pulito, organizzato e più facile da gestire.

Come indicato sopra, le classi astratte possono avere costruttori e le interfacce non possono, ma sono molto simili. Quando è consigliabile utilizzarle?

Quando utilizzi interfacce per scrivere una classe, la funzionalità di quest'ultima viene estesa tramite le istanze della classe che contiene. La composizione tende a semplificare il riutilizzo e sul motivo dell'ereditarietà da parte di una classe astratta. Inoltre, puoi utilizzare più interfacce in un corso, ma puoi eseguire la sottoclasse solo da una classe astratta.

La composizione spesso porta a un incapsulamento migliore, a un accoppiamento inferiore (interdipendenza), a interfacce più pulite e a un codice più utilizzabile. Per questi motivi, l'utilizzo della composizione con le interfacce è il design preferito. D'altra parte, l'ereditarietà da una classe astratta tende a essere la scelta più naturale per alcuni problemi. Perciò, è meglio preferire la composizione, ma se l'ereditarietà ha senso, Kotlin ti consente di farlo anche tu.

  • Utilizza un'interfaccia se hai molti metodi e una o due implementazioni predefinite, ad esempio in AquariumAction di seguito.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Puoi utilizzare una classe astratta ogni volta che non puoi completarla. Ad esempio, se torni alla classe AquariumFish, puoi rendere tutti gli strumenti AquariumFish implementati FishAction e fornire un'implementazione predefinita per eat, lasciando sfocato color, perché non c'è davvero 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 introduceva classi astratte, interfacce e l'idea della composizione. La delega dell'interfaccia è una tecnica avanzata in cui i metodi di un'interfaccia sono 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à di interfaccia necessaria a una classe helper separata e ognuna delle classi utilizza un'istanza della classe helper per implementare la funzionalità.

In questa attività, utilizzerai la delega dell'interfaccia per aggiungere funzionalità a un corso.

Passaggio 1: crea una nuova interfaccia

  1. In AquariumFish.kt, rimuovi la classe AquariumFish. Invece di ereditare dalla classe AquariumFish, Plecostomus e Shark implementeranno interfacce sia per l'azione pesci che per il 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 sostituire color da FishColor e eat() da FishAction.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Cambia la classe Shark per implementare 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 completato sarà 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 lezione su singleton

Ora implementerai la configurazione della parte della delega creando una classe helper che implementa FishColor. Crei una classe base chiamata GoldColor che implementa FishColor, il suo colore è oro.

Non ha senso creare più istanze di GoldColor, perché fanno tutte la stessa cosa. Pertanto, Kotlin ti consente di dichiarare una classe in cui è possibile crearne una sola istanza utilizzando la parola chiave object invece di class. Kotlin creerà questa istanza e vi farà riferimento il nome della classe. Quindi tutti gli altri oggetti possono utilizzare solo questa istanza: non è possibile creare altre istanze di questa classe. Se conosci il pattern singleton, ecco come implementare i singleton in Kotlin.

  1. In AquariumFish.kt, crea un oggetto per GoldColor. Sostituire il 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 prendere il colore da GoldColor. Per farlo, aggiungi by GoldColor alla dichiarazione del corso e crea la delega. Ciò significa che anziché utilizzare FishColor, utilizza l'implementazione fornita da GoldColor. Pertanto, ogni volta che si accede a color, viene delegata a GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

Con la classe così com'è, tutti i Plecos saranno dorati, ma questi pesci sono in realtà disponibili in molti colori. Per risolvere il problema, aggiungi un parametro costruttore per il colore con GoldColor come colore predefinito per Plecostomus.

  1. Modifica la classe Plecostomus in modo che passi un pass in fishColor con il costruttore e ne imposti il valore predefinito su GoldColor. Cambia 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

Analogamente, puoi utilizzare la delega dell'interfaccia per FishAction.

  1. In AquariumFish.kt crea una classe PrintingFishAction che implementi FishAction, che richiede un String, food, quindi stampa quello 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é verrà sostituita con una delega.
  2. Nella dichiarazione di Plecostomus, delega FishAction a PrintingFishAction, superando "eat algae".
  3. Con tutta la delega, non esiste alcun codice nel corpo della classe Plecostomus, quindi rimuovi {}, perché tutte le sostituzioni sono gestite 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 relative alla delega dell'implementazione.

La delega nell'interfaccia è potente e in genere dovresti considerare di usarla quando potresti utilizzare una classe astratta in un'altra lingua. Ti consente di utilizzare la composizione per collegare i comportamenti, invece di richiedere molte classi secondarie, ognuna delle quali è specializzata in un modo diverso.

Una classe di dati è simile a un struct in alcune altre lingue: esiste principalmente per contenere alcuni dati, ma un oggetto di classe di dati è ancora un oggetto. Gli oggetti Kotlin per la classe dati presentano alcuni vantaggi aggiuntivi, come le utilità per la stampa e la copia. In questa attività creerai una semplice classe dati e imparerai come il supporto fornito da Kotlin per le classi di dati.

Passaggio 1: crea una classe di dati

  1. Aggiungi un nuovo pacchetto decor nel pacchetto example.myapp per contenere il nuovo codice. Fai clic con il pulsante destro del mouse su example.myapp nel riquadro Progetto e seleziona File > Nuovo > pacchetto.
  2. Nel pacchetto, crea un nuovo corso chiamato Decoration.
package example.myapp.decor

class Decoration {
}
  1. Per rendere Decoration una classe di dati, aggiungi il prefisso data alla dichiarazione della classe.
  2. Aggiungi una proprietà String denominata rocks per fornire alla classe alcuni dati.
data class Decoration(val rocks: String) {
}
  1. Nel file, all'esterno della classe, aggiungi una funzione makeDecorations() per creare e stampare un'istanza di 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 ragionevole che viene creato perché si tratta di una classe di dati.
⇒ Decoration(rocks=granite)
  1. In makeDecorations(), crea un'istanza di altri due oggetti Decoration sia "quot;slate" e "stampa".
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 un secondo che confronti decoration3 con 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. Utilizza struttura

Per ottenere le proprietà di un oggetto di 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 organizzazione ed è un'abbreviazione 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 con 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 ignorarle utilizzando _ anziché il nome di una variabile, come mostrato nel codice di seguito.

    val (rock, _, diver) = d5

In questa attività imparerai alcune lezioni specifiche per Kotlin, tra cui:

  • Lezioni di singleton
  • Enum
  • Lezioni chiuse

Passaggio 1: richiama le lezioni singole

Ricordi l'esempio precedente con la classe GoldColor.

object GoldColor : FishColor {
   override val color = "gold"
}

Poiché ogni istanza di GoldColor funziona in modo analogo, viene dichiarata come object anziché come class per renderla singola. Può esserci solo un'istanza.

Passaggio 2: crea un'enumerazione

Kotlin supporta anche le enumerazioni, che consentono di enumerare gli elementi e farvi riferimento per nome, in modo simile alle altre lingue. Dichiara un'enumerazione preceduta dalla parola chiave enum. Per una dichiarazione di enumerazione di base è necessario 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);
}

Le enumerazioni sono come dei singleton, in quanto l'enumerazione di uno solo può essere solo uno dei due. Ad esempio, possono esistere un solo elemento Color.RED, Color.GREEN e Color.BLUE. In questo esempio, i valori RGB sono assegnati alla proprietà rgb per rappresentare i componenti di colore. Puoi anche ottenere il valore ordinale di un'enumerazione utilizzando la proprietà ordinal e il nome tramite 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 un corso chiuso

Una classe chiusa è una classe sottoclasse, ma solo all'interno del file in cui è stata dichiarata. Se provi a suddividere la classe in un file diverso, verrà visualizzato un errore.

Poiché classi e sottoclassi sono nello stesso file, Kotlin conoscerà tutte le sottoclassi in modo statico. In altre parole, al momento della compilazione, il compilatore vede tutte le classi e le sottoclassi e sa che sono tutte, in modo da poter eseguire ulteriori controlli.

  1. In AquariumFish.kt, prova un esempio di classe sigillata, tenendo presente 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 classificata in un altro file. Se vuoi aggiungere altri tipi di Seal, devi aggiungerli nello stesso file. In questo modo le classi sigillate sono un modo sicuro per rappresentare un numero fisso di tipi. Ad esempio, le classi sigillate sono un'ottima soluzione per restituire un errore o un errore a un'API di rete.

Questa lezione ha trattato molti argomenti. Gran parte di questo processo dovrebbe essere familiare da altri linguaggi di programmazione orientati agli oggetti, ma 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 principale direttamente nella definizione della classe. Ad esempio:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Se un costruttore principale necessita di codice aggiuntivo, scrivilo in uno o più blocchi init
  • Una classe può definire uno o più costruttori secondari utilizzando constructor, ma lo stile Kotlin consiste nell'utilizzare una funzione di fabbrica.

Modificatori e sottoclassi di visibilità

  • Tutte le classi e funzioni in Kotlin sono public per impostazione predefinita, ma puoi usare i modificatori per cambiare la visibilità in internal, private o protected.
  • Per creare una sottoclasse, la classe principale deve essere contrassegnata come open.
  • Per eseguire l'override di metodi e proprietà in una sottoclasse, i metodi e le proprietà devono essere contrassegnati come open nella classe principale.
  • Una classe sigillata può essere classificata solo nello stesso file in cui è definita. Crea una classe sigillata preceduta dalla dichiarazione sealed.

Classi di dati, singleton ed enumerazioni

  • Crea una classe di dati preceduta dalla dichiarazione data.
  • Struttura è un modo rapido per assegnare le proprietà di un oggetto data a variabili separate.
  • Crea una classe singleton utilizzando object anziché class.
  • Definisci un'enumerazione utilizzando enum class.

Classi, interfacce e delega astratte

  • Le classi e le interfacce astratte sono due modi per condividere il 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 alcuni o tutti i comportamenti.
  • Quando utilizzi interfacce per scrivere una classe, la funzionalità di quest'ultima viene estesa tramite le istanze della 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 per alcuni problemi.

Documentazione di Kotlin

Se vuoi saperne di più su qualsiasi argomento del corso o se ti blocchi, https://kotlinlang.org è il punto di partenza migliore.

Tutorial su Kotlin

Il sito web https://try.kotlinlang.org include tutorial avanzati 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 il Kotlin Bootcamp for Programrs.

IDA IntelliJ

La documentazione relativa a IntelliJ IDEA è disponibile sul sito web di JetBrains.

In questa sezione sono elencati i possibili compiti per gli studenti che lavorano attraverso questo codelab nell'ambito di un corso tenuto da un insegnante. Spetta all'insegnante fare quanto segue:

  • Assegna i compiti, se necessario.
  • Comunica agli studenti come inviare compiti.
  • Valuta i compiti.

Gli insegnanti possono utilizzare i suggerimenti solo quanto e come vogliono e dovrebbero assegnare i compiti che ritengono appropriati.

Se stai lavorando da solo a questo codelab, puoi utilizzare questi compiti per mettere alla prova le tue conoscenze.

Rispondi a queste domande

Domanda 1

I corsi hanno un metodo speciale che funge da progetto base per la creazione di oggetti di quella classe. Come si chiama il metodo?

▢ Un builder

▢ Un'istanza

▢ Un costruttore

▢ Un progetto base

Domanda 2

Quale delle seguenti affermazioni sulle interfacce e sulle classi astratte NON è corretta?

▢ Le classi astratte possono avere costruttori.

▢ Le interfacce non possono avere costruttori.

▢ Puoi creare un'istanza di interfacce e classi astratte direttamente.

▢ Le proprietà astratte devono essere implementate da sottoclassi della classe astratta.

Domanda 3

Quale dei seguenti NON è un modificatore di visibilità di 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 NON è un codice 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 possieda uno zoo con molti animali che devono essere curati tutti. Quale dei seguenti NON sarà parte dell'implementazione delle comunicazioni sanitarie?

▢ Un interface per diversi tipi di cibo.

▢ Un corso in abstract Caretaker da cui puoi creare diversi tipi di tutori.

▢ Un interface per dare acqua pulita a un animale.

▢ Una classe data per una voce di 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 Programs: Welcome to the Course."