程式設計人員的 Kotlin 新手上路課程 4:物件導向程式設計

這個程式碼研究室是「程式設計人員的 Kotlin 新手上路課程」的一部分。如果您按部就班完成程式碼研究室,就能充分體驗到本課程的價值。視您的知識多寡而定,您或許能略過某些部分。本課程適用於熟悉物件導向語言,且想學習 Kotlin 的程式設計師。

簡介

在本程式碼研究室中,您會建立 Kotlin 程式,並瞭解 Kotlin 中的類別和物件。如果您熟悉其他物件導向語言,應該會覺得這門課程的許多內容似曾相識,但 Kotlin 有一些重要差異,可減少您需要編寫的程式碼量。您也會瞭解抽象類別和介面委派。

本課程的設計目標是協助您累積知識,但各單元之間彼此半獨立,因此您可以略過熟悉的部分,不必建構單一範例應用程式。為將這些範例連結在一起,許多範例都使用水族館主題。如要查看完整的魚缸故事,請參閱 Udacity 的程式設計人員 Kotlin 新手上路課程

必備知識

  • Kotlin 的基本概念,包括型別、運算子和迴圈
  • Kotlin 的函式語法
  • 物件導向程式設計基礎知識
  • IntelliJ IDEA 或 Android Studio 等 IDE 的基本概念

課程內容

  • 如何在 Kotlin 中建立類別及存取屬性
  • 如何在 Kotlin 中建立及使用類別建構函式
  • 如何建立子類別,以及繼承的運作方式
  • 關於抽象類別、介面和介面委派
  • 如何建立及使用資料類別
  • 如何使用單例、列舉和密封類別

學習內容

  • 建立包含屬性的類別
  • 建立類別的建構函式
  • 建立子類別
  • 查看抽象類別和介面的範例
  • 建立簡單的資料類別
  • 瞭解單例項、列舉和密封類別

您應該已熟悉下列程式設計用語:

  • 類別是物件的藍圖,舉例來說,Aquarium 類別是製作水族箱物件的藍圖。
  • 物件是類別的執行個體,水族箱物件就是一個實際的 Aquarium
  • 屬性是類別的特徵,例如 Aquarium 的長度、寬度和高度。
  • 方法 (也稱為成員函式) 是類別的功能。方法是指您可對物件「執行」的動作。舉例來說,您可以 fillWithWater() Aquarium 物件。
  • 介面是類別可實作的規格。舉例來說,清潔是水族箱以外的物件常見的動作,而且不同物件的清潔方式通常類似。因此您可以建立名為 Clean 的介面,定義 clean() 方法。Aquarium 類別可以實作 Clean 介面,使用軟海綿清潔水族箱。
  • 套件可將相關程式碼分組,方便整理或製作程式碼程式庫。建立套件後,您可以將套件內容匯入其他檔案,並重複使用其中的程式碼和類別。

在這項工作中,您會建立新套件和類別,並加入一些屬性和方法。

步驟 1:建立套件

套件可協助您整理程式碼。

  1. 在「Project」窗格中,於「Hello Kotlin」專案下方的「src」資料夾上按一下滑鼠右鍵。
  2. 依序選取「New」(新增) >「Package」(套件),然後將其命名為 example.myapp

步驟 2:建立包含屬性的類別

類別是以 class 關鍵字定義,且類別名稱慣例會以大寫字母開頭。

  1. 在「example.myapp」example.myapp套件上按一下滑鼠右鍵。
  2. 依序選取「New」>「Kotlin File / Class」
  3. 在「Kind」下方選取「Class」,然後將類別命名為 Aquarium。IntelliJ IDEA 會在檔案中加入套件名稱,並為您建立空白的 Aquarium 類別。
  4. Aquarium 類別中,定義並初始化寬度、高度和長度 (以公分為單位) 的 var 屬性。使用預設值初始化屬性。
package example.myapp

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

在幕後,Kotlin 會自動為您在 Aquarium 類別中定義的屬性建立 getter 和 setter,因此您可以直接存取屬性,例如 myAquarium.length

步驟 3:建立 main() 函式

建立名為 main.kt 的新檔案,用來保存 main() 函式。

  1. 在左側的「Project」窗格中,對「example.myapp」套件按一下滑鼠右鍵。
  2. 依序選取「New」>「Kotlin File / Class」
  3. 在「類型」下拉式選單中,將選取項目保留為「檔案」,並將檔案命名為 main.kt。IntelliJ IDEA 會包含套件名稱,但不會包含檔案的類別定義。
  4. 定義 buildAquarium() 函式,並在其中建立 Aquarium 的例項。如要建立執行個體,請參照類別,就如同它是函式 Aquarium() 一樣。這會呼叫類別的建構函式,並建立 Aquarium 類別的例項,類似於在其他語言中使用 new
  5. 定義 main() 函式並呼叫 buildAquarium()
package example.myapp

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

fun main() {
    buildAquarium()
}

步驟 4:新增方法

  1. Aquarium 類別中,新增方法來列印水族箱的維度屬性。
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. main.ktbuildAquarium() 中,對 myAquarium 呼叫 printSize() 方法。
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. 按一下「main()」函式旁邊的綠色三角形,即可執行程式。查看結果。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. buildAquarium() 中,新增程式碼將高度設為 60,並列印變更後的維度屬性。
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. 執行程式並觀察輸出內容。
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

在這項工作中,您將為類別建立建構函式,並繼續使用屬性。

步驟 1:建立建構函式

在這個步驟中,您會將建構函式新增至第一個工作建立的 Aquarium 類別。在先前的範例中,每個 Aquarium 例項都是以相同尺寸建立。建立後,您可以設定屬性來變更尺寸,但一開始就建立正確大小的尺寸會比較簡單。

在某些程式設計語言中,建構函式的定義方式是在類別中建立與類別同名的方法。在 Kotlin 中,您可以在類別宣告本身直接定義建構函式,並在括號內指定參數,就像類別是方法一樣。與 Kotlin 中的函式一樣,這些參數可以包含預設值。

  1. 在您先前建立的 Aquarium 類別中,變更類別定義,加入三個建構函式參數,並為 lengthwidthheight 指派預設值,然後將這些參數指派給對應的屬性。
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. 更精簡的 Kotlin 做法是使用 varval,直接透過建構函式定義屬性,Kotlin 也會自動建立 getter 和 setter。接著,您就可以移除類別主體中的屬性定義。
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. 使用該建構函式建立 Aquarium 物件時,您可以不指定任何引數並取得預設值,也可以只指定部分引數,或指定所有引數並建立完全自訂大小的 Aquarium。在 buildAquarium() 函式中,嘗試使用具名參數建立 Aquarium 物件的不同方式。
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. 執行程式並觀察輸出內容。
⇒ 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 

請注意,您不必多載建構函式,也不必為每種情況 (以及其他組合) 編寫不同版本。Kotlin 會根據預設值和具名參數建立所需項目。

步驟 2:新增初始化區塊

上述範例建構函式只會宣告屬性,並將運算式的值指派給這些屬性。如果建構函式需要更多初始化程式碼,可以放在一或多個 init 區塊中。在這個步驟中,您要在 Aquarium 類別中新增一些 init 區塊。

  1. Aquarium 類別中,新增 init 區塊來輸出物件正在初始化,並新增第二個區塊來輸出以公升為單位的容量。
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. 執行程式並觀察輸出內容。
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 

請注意,系統會依 init 區塊在類別定義中的顯示順序執行這些區塊,且呼叫建構函式時會執行所有區塊。

步驟 3:瞭解次要建構函式

在本步驟中,您將瞭解次要建構函式,並將一個建構函式新增至類別。除了可包含一或多個 init 區塊的主要建構函式,Kotlin 類別也可以有一或多個次要建構函式,以允許建構函式多載,也就是具有不同引數的建構函式。

  1. Aquarium 類別中,使用 constructor 關鍵字新增次要建構函式,並將魚隻數量做為引數。根據魚隻數量,為以公升為單位的魚缸計算容量建立 val 罐屬性。假設每條魚需要 2 公升 (2,000 立方公分) 的水,並預留一些空間,以免水溢出。
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. 在次要建構函式中,請保持長度和寬度 (在主要建構函式中設定) 不變,並計算所需高度,使水箱達到指定容量。
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. buildAquarium() 函式中,新增呼叫來使用新的次要建構函式建立 Aquarium。顯示大小和音量。
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. 執行程式並觀察輸出內容。
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

請注意,音量會列印兩次,一次是由主要建構函式中的 init 區塊在執行次要建構函式前列印,另一次是由 buildAquarium() 中的程式碼列印。

您也可以在主要建構函式中加入 constructor 關鍵字,但大多數情況下不需要這麼做。

步驟 4:新增屬性 Getter

在這個步驟中,您要新增明確的屬性 Getter。定義屬性時,Kotlin 會自動定義 getter 和 setter,但有時需要調整或計算屬性的值。舉例來說,您在上方列印了 Aquarium 的體積。您可以定義變數和 getter,將音量設為屬性。由於 volume 需要計算,getter 必須傳回計算值,你可以使用單行函式執行這項操作。

  1. Aquarium 類別中,定義名為 volumeInt 屬性,並在下一行定義可計算音量的 get() 方法。
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. 移除會列印音量的 init 區塊。
  2. 移除 buildAquarium() 中列印音量的程式碼。
  3. printSize() 方法中,新增一行程式碼來列印音量。
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. 執行程式並觀察輸出內容。
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

維度和體積與先前相同,但物件由主要建構函式和次要建構函式完整初始化後,體積只會列印一次。

步驟 5:新增屬性設定器

在這個步驟中,您將為磁碟區建立新的屬性設定器。

  1. Aquarium 類別中,將 volume 變更為 var,這樣就能設定多次。
  2. 在 getter 下方新增 set() 方法,為 volume 屬性新增 setter,根據提供的水量重新計算高度。按照慣例,設定器參數的名稱為 value,但您也可以視需要變更。
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. buildAquarium() 中新增程式碼,將水族箱的容量設為 70 公升。顯示新大小。
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. 再次執行程式,並觀察高度和音量的變化。
⇒ 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

到目前為止,程式碼中沒有任何瀏覽權限修飾符,例如 publicprivate。這是因為 Kotlin 中的所有項目預設都是公開,也就是說,所有項目 (包括類別、方法、屬性和成員變數) 都可從任何位置存取。

在 Kotlin 中,類別、物件、介面、建構函式、函式、屬性及其 setter 可以有瀏覽權限修飾符

  • public 表示在類別外可見。根據預設,所有項目 (包括類別的變數和方法) 都是公開的。
  • internal 表示該函式只會在該模組內顯示。「模組」是一組一起編譯的 Kotlin 檔案,例如程式庫或應用程式。
  • private 表示該函式只會顯示在該類別中 (或您使用函式時的來源檔案中)。
  • protectedprivate 相同,但任何子類別也都能看到。

詳情請參閱 Kotlin 說明文件中的「可視度修飾符」。

成員變數

類別中的屬性或成員變數預設為 public。如果您使用 var 定義這些類別,這些類別就是可變動的,也就是可讀取及寫入。如果使用 val 定義,初始化後即為唯讀。

如果您希望程式碼可以讀取或寫入屬性,但外部程式碼只能讀取,可以將屬性和 getter 設為公開,並將 setter 宣告為私有,如下所示。

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

在這項工作中,您將瞭解 Kotlin 中的子類別和繼承機制。這些字詞與其他語言的字詞類似,但仍有部分差異。

在 Kotlin 中,類別預設無法加入子類別。同樣地,子類別無法覆寫屬性和成員變數 (但可以存取)。

您必須將類別標示為 open,才能加入子類別。同樣地,您必須將屬性和成員變數標示為 open,才能在子類別中覆寫這些項目。您必須使用 open 關鍵字,避免不慎將實作詳細資料洩漏為類別介面的一部分。

步驟 1:開放「水族館」類別

在這個步驟中,您會建立 Aquarium 類別 open,以便在下一個步驟中覆寫該類別。

  1. Aquarium 類別及其所有屬性標記 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. 新增開放式 shape 屬性,值為 "rectangle"
   open val shape = "rectangle"
  1. 新增具有 getter 的公開 water 屬性,該屬性會傳回 Aquarium 音量的 90%。
    open var water: Double = 0.0
        get() = volume * 0.9
  1. printSize() 方法中新增程式碼,列印形狀和水量 (以體積百分比表示)。
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. buildAquarium() 中,變更程式碼以建立含有 width = 25length = 25height = 40Aquarium
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. 執行程式並觀察新的輸出內容。
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

步驟 2:建立子類別

  1. 建立名為 TowerTankAquarium 子類別,實作圓柱形水箱,而非矩形水箱。您可以在 Aquarium 下方新增 TowerTank,因為您可以在與 Aquarium 類別相同的檔案中新增另一個類別。
  2. TowerTank 中,覆寫建構函式中定義的 height 屬性。如要覆寫屬性,請在子類別中使用 override 關鍵字。
  1. TowerTank 的建構函式接受 diameter。在 Aquarium 父類別中呼叫建構函式時,請同時使用 diameter 做為 lengthwidth
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. 覆寫 volume 屬性,計算圓柱體積。圓柱體的公式為圓周率乘以半徑的平方,再乘以高。您需要從 java.lang.Math 匯入常數 PI
    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. TowerTank 中,將 water 屬性覆寫為音量的 80%。
override var water = volume * 0.8
  1. shape 覆寫為 "cylinder"
override val shape = "cylinder"
  1. 最終的 TowerTank 類別應如下所示。

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. buildAquarium() 中建立 TowerTank,直徑為 25 公分,高度為 45 公分。顯示大小。

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. 執行程式並觀察輸出內容。
⇒ 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)

有時您會想定義通用行為或屬性,以便在某些相關類別之間共用。Kotlin 提供兩種方法:介面和抽象類別。在這項工作中,您將為所有魚類共有的屬性建立抽象 AquariumFish 類別。您建立名為 FishAction 的介面,定義所有魚類通用的行為。

  • 抽象類別和介面都無法自行例項化,也就是說,您無法直接建立這些型別的物件。
  • 抽象類別有建構函式。
  • 介面無法包含任何建構函式邏輯或儲存任何狀態。

步驟 1:建立抽象類別

  1. example.myapp 下方,建立新檔案 AquariumFish.kt
  2. 建立名為 AquariumFish 的類別,並標示為 abstract
  3. 新增一個 String 屬性 (color),並標示 abstract
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. 建立 AquariumFish 的兩個子類別:SharkPlecostomus
  2. 由於 color 是抽象類別,因此子類別必須實作。將 Shark 設為灰色,Plecostomus 設為金色。
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. main.kt 中,建立 makeFish() 函式來測試類別。例項化 SharkPlecostomus,然後列印每個項目的顏色。
  2. 刪除 main() 中先前的測試程式碼,並新增對 makeFish() 的呼叫。程式碼應如下所示。

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. 執行程式並觀察輸出內容。
⇒ Shark: gray 
Plecostomus: gold

下圖代表 Shark 類別和 Plecostomus 類別,這兩個類別會將抽象類別 AquariumFish 子類別化。

這張圖表顯示抽象類別 AquariumFish,以及 Shark 和 Plecostumus 這兩個子類別。

步驟 2:建立介面

  1. AquariumFish.kt 中,建立名為 FishAction 的介面,其中包含 eat() 方法。
interface FishAction  {
    fun eat()
}
  1. 在每個子類別中新增 FishAction,並實作 eat(),讓它輸出魚的動作。
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. makeFish() 函式中,呼叫 eat(),讓您建立的每條魚吃東西。
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. 執行程式並觀察輸出內容。
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

下圖代表 Shark 類別和 Plecostomus 類別,這兩個類別都由 FishAction 介面組成並實作該介面。

抽象類別與介面的使用時機

上述範例很簡單,但如果您有許多相互關聯的類別,抽象類別和介面可協助您保持設計簡潔、更有條理,且更容易維護。

如上所述,抽象類別可以有建構函式,但介面不行,除此之外,兩者非常相似。那麼,您應該在什麼時候使用哪一種呢?

使用介面組合類別時,類別所含的類別例項會擴充類別的功能。相較於從抽象類別繼承,組合通常更容易重複使用程式碼和進行推理。此外,您可以在類別中使用多個介面,但只能從一個抽象類別建立子類別。

組合通常可帶來更完善的封裝、更低的耦合 (相互依賴)、更簡潔的介面,以及更實用的程式碼。基於這些原因,使用介面組合是較好的設計。另一方面,從抽象類別繼承往往是某些問題的自然解決方式。因此您應優先使用組合,但如果繼承有意義,Kotlin 也允許您這麼做!

  • 如果您有很多方法和一或兩種預設實作項目,請使用介面,例如下方的 AquariumAction
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • 只要無法完成類別,就可以使用抽象類別。舉例來說,回到 AquariumFish 類別,您可以讓所有 AquariumFish 實作 FishAction,並為 eat 提供預設實作,同時將 color 設為抽象,因為魚類沒有預設顏色。
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

先前的練習介紹了抽象類別、介面和組合的概念。介面委派是一種進階技術,介面的方法是由輔助 (或委派) 物件實作,然後由類別使用。在一連串不相關的類別中使用介面時,這個技巧就非常實用:您可以在個別的輔助類別中新增所需的介面功能,而每個類別都會使用輔助類別的執行個體來實作功能。

在這項工作中,您會使用介面委派將功能新增至類別。

步驟 1:建立新介面

  1. AquariumFish.kt 中,移除 AquariumFish 類別。Plecostomus 和  不會從 AquariumFish 類別繼承,而是會實作魚類動作和顏色的介面。Shark
  2. 建立新的介面 FishColor,將顏色定義為字串。
interface FishColor {
    val color: String
}
  1. 變更 Plecostomus,實作兩個介面 FishActionFishColor。您需要從 FishColor 覆寫 color,並從 FishAction 覆寫 eat()
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Shark 類別變更為同時實作 FishActionFishColor 這兩個介面,而非從 AquariumFish 繼承。
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. 完成的程式碼應如下所示:
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")
    }
}

步驟 2:建立單例類別

接著,請建立導入 FishColor 的輔助程式類別,實作委派部分的設定。您建立名為 GoldColor 的基本類別,實作 FishColor,只會說明其顏色為金色。

建立多個 GoldColor 執行個體沒有意義,因為這些執行個體執行的動作完全相同。因此,Kotlin 可讓您宣告類別,且只能使用 object 關鍵字 (而非 class) 建立一個例項。Kotlin 會建立該執行個體,而該執行個體會由類別名稱參照。然後所有其他物件都可以只使用這個例項,因為無法建立這個類別的其他例項。如果您熟悉單例模式,這就是在 Kotlin 中實作單例模式的方式。

  1. AquariumFish.kt 中,為 GoldColor 建立物件。覆寫顏色。
object GoldColor : FishColor {
   override val color = "gold"
}

步驟 3:為 FishColor 新增介面委派

現在您已準備好使用介面委派。

  1. AquariumFish.kt 中,從 Plecostomus 移除 color 的覆寫。
  2. 變更 Plecostomus 類別,從 GoldColor 取得顏色。方法是在類別宣告中加入 by GoldColor,然後建立委派。這表示請使用 GoldColor 提供的實作項目,而非實作 FishColor。因此每次存取 color 時,都會委派給 GoldColor
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

以目前的類別來說,所有異形魚都會是金色的,但這些魚其實有許多顏色。如要解決這個問題,請為顏色新增建構函式參數,並將 GoldColor 設為 Plecostomus 的預設顏色。

  1. 變更 Plecostomus 類別,以透過建構函式採用傳入的 fishColor,並將預設值設為 GoldColor。將委派項目從 by GoldColor 變更為 by fishColor
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

步驟 4:為 FishAction 新增介面委派

同樣地,您也可以對 FishAction 使用介面委派。

  1. AquariumFish.kt 中,建立實作 FishActionPrintingFishAction 類別,該類別會採用 Stringfood,然後輸出魚類吃的食物。
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. Plecostomus 類別中,移除覆寫函式 eat(),因為您將以委派項目取代該函式。
  2. Plecostomus 的宣告中,將 FishAction 委派給 PrintingFishAction,並傳遞 "eat algae"
  3. 完成所有委派作業後,Plecostomus 類別主體中就不會有任何程式碼,因此請移除 {},因為所有覆寫作業都會由介面委派處理
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

下圖代表 SharkPlecostomus 類別,兩者都由 PrintingFishActionFishColor 介面組成,但會將實作項目委派給這些介面。

介面委派功能十分強大,一般來說,只要您可能在其他語言中使用抽象類別,就應考慮如何使用這項功能。您可以使用組合插入行為,而不需大量子類別,每個子類別都以不同方式專門化。

資料類別與其他語言中的 struct 類似,主要用於保存資料,但資料類別物件仍是物件。Kotlin 資料類別物件可帶來一些額外優勢,例如用於列印和複製的公用程式。在這項工作中,您會建立簡單的資料類別,並瞭解 Kotlin 對資料類別提供的支援。

步驟 1:建立資料類別

  1. example.myapp 套件下新增 decor 套件,用於存放新程式碼。在「Project」窗格中,對「example.myapp」按一下滑鼠右鍵,然後依序選取「File」>「New」>「Package」
  2. 在套件中,建立名為 Decoration 的新類別。
package example.myapp.decor

class Decoration {
}
  1. 如要將 Decoration 設為資料類別,請在類別宣告前加上 data 關鍵字。
  2. 新增名為 rocksString 屬性,為類別提供一些資料。
data class Decoration(val rocks: String) {
}
  1. 在檔案中,於類別外新增 makeDecorations() 函式,以建立及列印 Decoration 的例項 (含 "granite")。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. 新增 main() 函式來呼叫 makeDecorations(),然後執行程式。請注意,由於這是資料類別,因此會建立合理的輸出內容。
⇒ Decoration(rocks=granite)
  1. makeDecorations() 中,再例項化兩個 Decoration 物件 (皆為「slate」),然後列印這些物件。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. makeDecorations() 中,新增輸出陳述式,輸出比較 decoration1decoration2 的結果,以及比較 decoration3decoration2 的結果。使用資料類別提供的 equals() 方法。
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. 執行程式碼。
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

步驟 2:使用解構

如要取得資料物件的屬性並指派給變數,可以一次指派一個屬性,如下所示。

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

您可以改為為每個屬性建立變數,然後將資料物件指派給變數群組。Kotlin 會將屬性值放入每個變數。

val (rock, wood, diver) = decoration

這稱為「解構」,是實用的簡寫方式。變數數量應與屬性數量相符,且變數會按照類別中宣告的順序指派。以下是完整的範例,您可以在 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

如果不需要一或多個屬性,可以使用 _ (而非變數名稱) 略過這些屬性,如下列程式碼所示。

    val (rock, _, diver) = d5

在本工作中,您將瞭解 Kotlin 中的一些專用類別,包括:

  • 單例模式類別
  • 列舉
  • 密封類別

步驟 1:回想單例模式類別

回想一下先前 GoldColor 類別的範例。

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

由於每個 GoldColor 執行個體都會執行相同作業,因此會宣告為 object,而非 class,藉此設為單例模式。只能有一個執行個體。

步驟 2:建立列舉

Kotlin 也支援列舉,可讓您列舉項目並依名稱參照,與其他語言類似。在宣告前加上 enum 關鍵字,即可宣告列舉。基本列舉宣告只需要名稱清單,但您也可以定義與每個名稱相關聯的一或多個欄位。

  1. Decoration.kt 中,試用列舉的範例。
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

列舉與單例模式有點類似,只能有一個,且列舉中的每個值只能有一個。舉例來說,只能有一個 Color.RED、一個 Color.GREEN 和一個 Color.BLUE。在本範例中,RGB 值會指派給 rgb 屬性,代表顏色元件。您也可以使用 ordinal 屬性取得列舉的序數值,並使用 name 屬性取得列舉的名稱。

  1. 請試試另一個列舉範例。
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

步驟 3:建立密封類別

密封類別可以加入子類別,但只能在宣告類別的檔案內加入。如果您嘗試在其他檔案中將類別設為子類別,就會收到錯誤訊息。

由於類別和子類別位於同一個檔案中,Kotlin 會靜態瞭解所有子類別。也就是說,在編譯期間,編譯器會看到所有類別和子類別,並知道這些就是全部的類別,因此編譯器可以為您執行額外檢查。

  1. AquariumFish.kt 中,試用密封類別的範例,並維持水生主題。
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

Seal 類別無法在其他檔案中建立子類別。如要新增更多 Seal 類型,必須在同一個檔案中新增。因此密封類別是代表固定數量型別的安全方式。舉例來說,密封類別非常適合從網路 API 傳回成功或錯誤

本課程涵蓋許多主題,雖然 Kotlin 的許多概念與其他物件導向程式設計語言類似,但 Kotlin 新增了一些功能,可讓程式碼保持簡潔易讀。

類別和建構函式

  • 使用 class 在 Kotlin 中定義類別。
  • Kotlin 會自動為屬性建立 setter 和 getter。
  • 直接在類別定義中定義主要建構函式。例如:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • 如果主要建構函式需要額外程式碼,請在一或多個 init 區塊中撰寫。
  • 類別可以使用 constructor 定義一或多個次要建構函式,但 Kotlin 樣式是改用工廠函式。

瀏覽權限修飾符和子類別

  • Kotlin 中的所有類別和函式預設為 public,但您可以使用修飾符將瀏覽權限變更為 internalprivateprotected
  • 如要建立子類別,父項類別必須標示為 open
  • 如要在子類別中覆寫方法和屬性,必須在父項類別中將方法和屬性標示為 open
  • 密封類別只能在定義所在的檔案中建立子類別。在宣告前加上 sealed,即可建立密封類別。

資料類別、單例模式和列舉

  • 在宣告前加上 data,即可建立資料類別。
  • 解構是將 data 物件的屬性指派給個別變數的簡寫。
  • 請使用 object 而非 class,建立單例類別。
  • 使用 enum class 定義列舉。

抽象類別、介面和委派

  • 抽象類別和介面是兩種在類別之間共用常見行為的方式。
  • 抽象類別會定義屬性和行為,但實作內容則留給子類別。
  • 介面會定義行為,並可能為部分或所有行為提供預設實作內容。
  • 使用介面組合類別時,類別所含的類別例項會擴充類別的功能。
  • 介面委派會使用組合,但也會將實作項目委派給介面類別。
  • 組合是透過介面委派將功能新增至類別的強大方式。一般來說,組合是較好的做法,但有些問題更適合從抽象類別繼承。

Kotlin 說明文件

如要進一步瞭解本課程的任何主題,或遇到問題,請前往 https://kotlinlang.org

Kotlin 教學課程

https://try.kotlinlang.org 網站提供豐富的教學課程 (稱為 Kotlin Koans)、網頁式解譯器,以及附有範例的完整參考文件。

Udacity 課程

如要查看這個主題的 Udacity 課程,請參閱「程式設計人員的 Kotlin 新手上路課程」。

IntelliJ IDEA

IntelliJ IDEA 的說明文件位於 JetBrains 網站。

本節列出的作業可由課程講師指派給學習本程式碼研究室的學員。講師可自由採取以下行動:

  • 視需要指派作業。
  • 告知學員如何繳交作業。
  • 為作業評分。

講師可以視需求使用全部或部分建議內容,也可以自由指派任何其他合適的作業。

如果您是自行學習本程式碼研究室,不妨利用這些作業驗收學習成果。

回答問題

第 1 題

類別具有特殊方法,可做為建立該類別物件的藍圖。這個方法稱為?

▢ 建構工具

▢ 執行個體化工具

▢ 建構函式

▢ 藍圖

第 2 題

下列有關介面和抽象類別的敘述何者不正確?

▢ 抽象類別可以有建構函式。

▢ 介面不得有建構函式。

▢ 介面和抽象類別可以直接例項化。

▢ 抽象屬性必須由抽象類別的子類別實作。

第 3 題

下列何者「不是」Kotlin 屬性、方法等的瀏覽權限修飾符?

internal

nosubclass

protected

private

第 4 題

請考慮這個資料類別:
data class Fish(val name: String, val species:String, val colors:String)
以下哪一個程式碼「不」正確,無法建立及解構 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")

第 5 題

假設您經營動物園,園內有許多動物需要照料。下列何者「不」屬於照護實施範圍?

interface動物吃的不同食物類型。

abstract Caretaker 類別,可從中建立不同類型的照護者。

interface為動物提供乾淨的水。

▢ 餵食時間表項目的 data 類別。

繼續下一個課程:5.1 擴充功能

如要查看課程總覽,包括其他程式碼研究室的連結,請參閱「程式設計人員的 Kotlin 新手上路課程:歡迎參加本課程。