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

本程式碼研究室是程式設計課程 Kotlin 新手上路課程的一部分。使用程式碼研究室逐步完成程式碼課程後,您將能充分發揮本課程的潛能。視您的知識而定,您或許可以略過某些部分。本課程的適用對象為熟悉物件導向語言,且想要學習 Kotlin 的程式設計人員。

簡介

在這個程式碼研究室,您可以建立 Kotlin 程式,並學習 Kotlin 中的類別和物件。如果您知道另一種物件導向語言,大部分內容對您來說就不陌生,但 Kotlin 有一些重要差異,可以減少您需要撰寫的程式碼數量。此外,您也會瞭解抽象類別和介面委派。

本課程並非只建立一個範例應用程式,而是用來建立您的知識,但彼此之間互不相關,因此您可以熟悉自己熟悉的部分。許多產品是透過水族箱主題進行連結。如果您想看完整的水族箱故事,請參考程式設計程式 Kotlin 新手上路課程

須知事項

  • Kotlin 基本概念,包括類型、運算子和迴圈
  • Kotlin's 函式語法
  • 物件導向程式設計的基本概念
  • IDE 的基本概念,例如 IntelliJ IDEA 或 Android Studio

課程內容

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

執行步驟

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

您應該已經熟悉以下的計劃條款:

  • 類別是物件的藍圖。舉例來說,Aquarium 類別是製作水族館物件的藍圖。
  • 物件是類別的執行個體,水族箱物件則是一個實際的 Aquarium
  • 屬性是類別的特徵,例如 Aquarium 的長度、寬度與高度。
  • Methods (也稱為 member 函式) 是 類別的功能。方法就是您能夠「做到」的事物。例如,您可以fillWithWater() Aquarium 物件。
  • 「介面」是指類別可實作的規格。舉例來說,水族箱以外的物品常有清潔,而一般用不同方式處理的方式通常也不一樣。因此,您可能有一個稱為 Clean 的介面,用於定義 clean() 方法。Aquarium 類別可實作 Clean 介面以使用海綿來清潔水族箱。
  • 套件可用來將相關的程式碼分組,讓內容井然有序,或建立程式碼庫。建立套件後,您可以將套件的內容匯入其他檔案,並重複使用當中的程式碼和類別。

在這項工作中,您會建立一個新的套件,以及包含一些屬性和 方法的類別。

步驟 1:建立套件

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

  1. 在「Project」(專案) 窗格中,於「Hello Kotlin」專案底下的 [src] 資料夾上按一下滑鼠右鍵。
  2. 選取 [New > Package] 並呼叫 example.myapp

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

類別是透過關鍵字 class 定義,而一般按照類別名稱開頭加上大寫字母。

  1. 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. 在「Kind」下拉式選單中,保留選取項目 [File],並將檔案命名為 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.kt 中,在 buildAquarium() 上呼叫 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 區塊中。在此步驟中,您將部分 init 區塊新增至 Aquarium 類別。

  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 cm^3),外加一個額外的空氣,所以水不溢出。
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 中,類別、物件、介面、建構函式、函式、屬性和其設定器可能具備顯示設定修飾符

  • public 表示課程外的顯示設定。所有項目預設為公開,包含類別的變數和方法。
  • internal 表示該模組只會在該模組內顯示。「模組」是一組編譯的 Kotlin 檔案,例如程式庫或應用程式。
  • private 表示該類別僅會顯示在該類別 (如果您使用函式時則為來源檔案)。
  • protected」與「private」相同,但所有子類別都可以看到這個類別。

詳情請參閱 Kotlin 文件中的瀏覽權限修改工具

成員變數

類別或成員變數中的屬性預設為 public。如果您透過 var 定義這些變數,其可變動,亦即具有可讀性與可寫入。如果您使用 val 定義這些變數,這些變數在初始化後為唯讀狀態。

如需程式碼可讀取或寫入的資源,但外部程式碼只能讀取,您可以將屬性及其 getter 設為公開,並宣告設定器為私人,如下所示。

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. open 關鍵字標記 Aquarium 類別及其所有屬性。
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. 新增值為 "rectangle" 的開放式 shape 屬性。
   open val shape = "rectangle"
  1. 添加一個打開的 water 包含的 getter 和返回的 Aquarium 卷的 90% 的 getter。
    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. 建立名為 Aquarium 的子類別,這個子類別會導入圓形圓柱槽,而非矩形槽。您可以在Aquarium下方加入 TowerTank,因為您可以在另一個類別中加入與 Aquarium 類別相同的類別。
  2. TowerTank 中,覆寫建構函式中定義的 height 屬性。若要覆寫屬性,請使用子類別中的 override 關鍵字。
  1. TowerTank 的建構函式使用 diameter。在 Aquarium 父類別中呼叫建構函式時,請針對 diameterwidth 使用 diameter
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. 覆寫磁碟區屬性以計算氣瓶。圓柱的公式是半徑的平方,再乘以高。您必須從 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() 建立一個直徑 25 公分、高 45 公分的TowerTank。列印尺寸。

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. 建立 AquariumFishSharkPlecostomus 這兩個子類別。
  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 的子類別。

呈現「SquarkFish」的抽象類別「SquareFish」和「Subleumus」兩個子類別。

步驟 2:建立介面

  1. AquariumFish.kt 中,使用 eat() 方法建立名為 FishAction 的介面。
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
interface FishAction  {
    fun eat()
}

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

前一項工作介紹了抽象類別、介面和構思概念。「介面委派」是一種先進的技術,可透過輔助 (或委派) 物件實作介面,隨後由類別使用。當您在一個不相關的類別中使用介面時,這項技巧就很實用:您將所需的介面功能加到個別輔助程式類別,而每個類別都使用輔助類別的執行個體來實作功能。

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

步驟 1:建立新介面

  1. AquariumFish.kt 中,移除 AquariumFish 類別。PlecostomusShark 不會沿用 AquariumFish 類別的實作,而是針對魚類動作及其顏色實作介面。
  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")
   }
}

目前我們以 Plecos 的 140 級標示「Plecos」,因此所有魚類都會變為金色,但這些魚實際上的色彩相當多。您可以通過添加一個以 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 中建立一個 PrintingFishAction 類別,實作 FishAction,該類別會列印 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 >新增 > 套件
  2. 在套件中,建立名為「Decoration」的新類別。
package example.myapp.decor

class Decoration {
}
  1. 如要將 Decoration 設為資料類別,請在類別宣告前面加上關鍵字 data
  2. 加入名為 rocksString 屬性,為全班提供部分資料。
data class Decoration(val rocks: String) {
}
  1. 將檔案新增至類別外的 makeDecorations() 函式,以使用 "granite" 建立及列印 Decoration 的執行個體。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. 新增可呼叫 makeDecorations()main() 函式並執行程式。請留意系統建立的合理輸出,因為這是資料類別。
⇒ Decoration(rocks=granite)
  1. makeDecorations() 中,將另外兩個 Decoration 物件執行個體化為「插入畫面」,並列印這些物件。
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

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

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. makeDecorations() 中,新增列印聲明,列印出「decoration1」與「decoration2」的比較結果,再新增第二個對帳單,比較 decoration3decoration2。使用資料類別提供的等於。
    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 會自動為屬性建立 setter 和 getter。
  • 直接在類別定義中定義主要建構函式。例如:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • 如果主要建構函式需要額外的程式碼,請以一或多個 init 區塊的形式編寫。
  • 類別可使用 constructor 定義一或多個次要建構函式,但 Kotlin 樣式是使用 Factory 函式。

可見度修飾符和子類別

  • 根據預設,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、能見度等項目的 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 新手課程:歡迎參加這堂課程。