Kotlin 中的類別和繼承

1. 事前準備

必要條件

  • 熟悉使用 Kotlin Playground 編輯 Kotlin 程式。
  • 如本課程單元 1 所述,瞭解 Kotlin 程式設計的基本概念。具體來說,您可學到 main() 程式、會傳回值、變數、資料類型和作業的含引數函式,以及 if/else 陳述式。
  • 您還可以在 Kotlin 中定義類別、從當中建立物件執行個體,以及存取類別的屬性和方法。

課程內容

  • 建立 Kotlin 程式,並使用繼承實作類別階層。
  • 擴充類別、覆寫現有的功能,並新增功能。
  • 選擇變數適用的適當瀏覽權限修飾符。

建構項目

  • Kotlin 程式包含做為類別階層實作的各種居所。

需求條件

2. 類別階層是什麼?

使用者習慣為類似的屬性和行為項目分門別類,甚至排列一些類型的階層。舉例來說,在蔬菜等廣泛的類別下,可以細分為豆類等類型。豆類下可以再細分為豌豆、四季豆、扁豆、鷹嘴豆、大豆等。

而這可用階層表示,因為豆類包含或沿用蔬菜的所有屬性 (例如兩者均為可食用植物)。以此類推,豌豆、四季豆、扁豆都有豆類的屬性,外加豆類獨有的屬性。

以下說明如何以程式設計術語表示上述關係。如果在 Kotlin 中將 Vegetable 設為類別,即可建立 Legume 做為子項,或 Vegetable 類別的子類別。即 Legume 類別會繼承 (也可使用) Vegetable 類別所有的屬性和方法。

以下的類別階層圖表可顯示這項關係。您可以參照 Vegetable 做為 Legume 類別的父項或父類別

87e0a5eb0f85042d.png

您可以建立 Legume 的子類別 (例如 LentilChickpea),然後繼續展開類別階層。這會將 Legume 設為 Vegetable 的子項或子類別,同時也設為 LentilChickpea 的父項或父類別。Vegetable 是這個階層的「根」層級或頂層 (或基礎) 類別。

638655b960530d9.png

Android 類別中的繼承

雖然不使用類別,也可以撰寫 Kotlin 程式碼,但在之前的程式碼研究室中,Android 以類別的格式提供很多組件,包括活動、檢視畫面和檢視區塊群組。所以,瞭解類別階層是 Android 應用程式開發的基礎,讓您可以善用 Android 架構提供的功能。

舉例來說,Android 的 View 類別代表螢幕上負責繪圖和事件處理的長方形區域。TextView 類別是 View 類別的子類別,代表 TextView 繼承 View 類別的所有屬性和功能,並新增向使用者顯示文字的特定邏輯。

c39a8aaa5b013de8.png

更進一步來說,EditTextButton 類別是 TextView 類別的子項。這些類別繼承 TextViewView 類別所有的屬性和方法,並新增專屬的特定邏輯。例如,EditText 加入可編輯螢幕上文字的專屬功能。

EditText 可以直接將 TextView 類別加入子類別,同時將 View 類別加入子類別,不必在 EditText 類別複製及貼上 ViewTextView 類別的所有邏輯。然後,EditText 類別中的程式碼可以致力讓 UI 元件不同於其他檢視畫面。

在 developers.android.com 網站 Android 類別的文件資訊頁面頂端上,即可找到類別階層圖。如果您在階層頂端看到 kotlin.Any,原因是 Kotlin 中所有類別都有相同的父類別「Any」。請按這裡瞭解詳情。

1ce2b1646b8064ab.png

如您所見,學習利用類別之間的繼承知識,可讓您更輕鬆地撰寫、重複使用、讀取並測試程式碼。

3. 建立基礎類別

居所類別階層

在本程式碼研究室中,您將建構 Kotlin 程式,該程式使用居所 (一般人起居的遮蔽處) 和樓層空間、樓層和居民做為範例,示範類別階層的運作方式。

以下是您要建構的類別階層圖。根層級的 Dwelling 會指定居所為 true 的屬性和功能 (類似於藍圖)。接著,您會看到方形小屋 (SquareCabin)、圓型茅屋 (RoundHut),以及圓塔 (RoundTower,也就是多樓層的 RoundHut)。

de1387ca7fc26c81.png

實作的類別:

  • Dwelling:代表非特定遮蔽處的基本類別,包含所有居所通用資訊。
  • SquareCabin:正方形樓層面積的木製正方形小屋。
  • RoundHut:茅草建造的圓形茅屋,包含圓形樓層面積和 RoundTower 的父項。
  • RoundTower:石頭建造的圓塔,包含圓形樓層面積和多個樓層。

建立抽象居所類別

任何類別都可以是類別階層的基本類別,或其他類別的父項。

「abstract」(抽象) 類別是因為未完整實作,無法執行個體化的類別。您可以將其視為草圖。草圖包含專案的靈感和規劃,但往往缺少資訊建構專案。請使用草圖 (抽象類別) 建立藍圖 (類別),然後建構實際的物件執行個體。

建立父類別常見的好處是,包含所有子類別通用的屬性和函式。如果不知道屬性的值和函式的實作,請將類別設為抽象。舉例來說,Vegetables 包含許多所有蔬菜通用的屬性,但您無法為非特定蔬菜建立執行個體,因為您沒有具體資訊,例如形狀或顏色。因此,Vegetable 是抽象類別,由子類別確定每種蔬菜的特定詳細資料。

抽象類別的宣告是以 abstract 關鍵字做為開頭。

Dwelling 是如 Vegetable 的抽象類別,它會包含很多類型的居所通用的屬性和函式,但不知道屬性確切的值和函式實作的詳細資料。

  1. 前往 Kotlin Playground:https://developer.android.com/training/kotlinplayground
  2. 在編輯器中,刪除 main() 函式中的 println("Hello, world!")
  3. 然後在 main() 函式下方加入以下程式碼,建立名為 Dwellingabstract 類別。
abstract class Dwelling(){
}

新增建材的屬性

在這個 Dwelling 類別中,您可以定義所有居所為 true 的建材,即使建材會因不同的居所而異。所有居所都使用建材建構。

  1. Dwelling 中,建立類型 StringbuildingMaterial 變數以代表建材。因為建材不會變更,請使用 val 做為建材不可變的變數。
val buildingMaterial: String
  1. 執行程式後,您會看到以下錯誤訊息。
Property must be initialized or be abstract

buildingMaterial 屬性沒有值。其實您「無法」指定值,因為非特定的建築物不會使用任何特定建材。所以如錯誤訊息所示,您可以在 buildingMaterial 的宣告中前置 abstract 關鍵字,表示不會在這裡定義此屬性。

  1. 在變數定義中加入 abstract 關鍵字。
abstract val buildingMaterial: String
  1. 執行程式碼時,只要執行不出現任何錯誤,編譯程式碼就不會出現錯誤。
  2. main() 函式中建立 Dwelling 的執行個體,並執行程式碼。
val dwelling = Dwelling()
  1. 您看到錯誤訊息的原因是,無法建立抽象 Dwelling 類別的執行個體。
Cannot create an instance of an abstract class
  1. 刪除不正確的程式碼。

您目前完成的程式碼:

abstract class Dwelling(){
    abstract val buildingMaterial: String
}

新增容量屬性

居所的另一個屬性是居住的人數。

所有居所的容量都不會變更。但 Dwelling 父類別中無法設定容量。如果是特定類型的居所,請在子類別定義容量。

  1. Dwelling 中,新增名為 capacityabstract 整數 val
abstract val capacity: Int

新增居民數目的私有屬性

所有居所都有很多 residents 居住在其中 (數目可能小於或等於 capacity),所以為了繼承並使用屬性,請定義所有子類別 Dwelling 中的 residents 屬性。

  1. 您可以將 residents 參數傳遞至 Dwelling 類別建構函式。residents 屬性為 var,因為建立執行個體後,可以變更居民數目。
abstract class Dwelling(private var residents: Int) {

請注意,residents 屬性會標示 private 關鍵字。Private (私有) 是 Kotlin 的可見度修飾符,即只有這個類別能看到 (並使用) residents 屬性。程式其他位置無法存取此屬性。您可以使用 private 關鍵字標示屬性或方法。否則,在未指定瀏覽權限修飾符時,這些屬性和方法會預設為 public,並可透過程式的其他位置存取。有鑑於居所的居民數目通常是私人資訊 (相對於建材或建築物容量),這樣的決定很合理。

使用居所的 capacity 和目前 residents 定義的數目,您可以建立函式 hasRoom(),判斷居所是否空房容納其他居民。您可以在 Dwelling 類別中定義並實作 hasRoom() 函式,因為計算所有居所是否有空房,使用的是相同的公式。如果 residents 的數目小於 capacityDwelling 中會顯示空房,然後此函式會根據比較值傳回 truefalse

  1. 新增 hasRoom() 函式至 Dwelling 類別。
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. 請執行這個程式碼,此程式碼不該出現錯誤。目前未出現任何動作。

完成的程式碼如下:

abstract class Dwelling(private var residents: Int) {

   abstract val buildingMaterial: String
   abstract val capacity: Int

   fun hasRoom(): Boolean {
       return residents < capacity
   }
}

4. 建立子類別

建立 SquareCabin 子類別

  1. Dwelling 類別下方,建立名為 SquareCabin 的類別。
class SquareCabin
  1. 接著,您必須指出 SquareCabinDwelling 的關連。在程式碼中,您要指出 SquareCabinDwelling 的延伸模組 (或 Dwelling) 的子類別,因為 SquareCabin 會提供 Dwelling 抽象部分的實作)。

若要指出繼承關係,請在 SquareCabin 類別名稱後方加上半形冒號 (:),然後呼叫並初始化父項 Dwelling 類別。別忘了在 Dwelling 類別名稱後方加上括號。

class SquareCabin : Dwelling()
  1. 從父類別延伸時,您必須傳入父類別必要的參數。Dwelling 要求 residents 數目做為輸入內容。您可以傳入居民的固定數目 (例如 3)。
class SquareCabin : Dwelling(3)

但若要程式更有彈性,並可以使用 SquareCabins 居民的變數數目,請在 SquareCabin 類別定義中,將 residents 設為參數。請勿將 residents 宣告為 val,,因為您會重複使用父項類別 Dwelling 中宣告的屬性。

class SquareCabin(residents: Int) : Dwelling(residents)
  1. 執行程式碼。
  2. 這會導致錯誤發生。一探究竟:
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling

宣告抽象函式和變數和 promise 一樣,您會在之後提供值並實作。以變數來說,即代表您必須提供值給抽象類別的子類別。以函式來說,即子類別必須實作函式主體。

Dwelling 類別中,您已定義 abstract 變數為 buildingMaterialSquareCabinDwelling 的子類別,所以必須提供 buildingMaterial 的值。使用 override 關鍵字,指出父項類別已定義這個屬性,並會在此類別中覆寫。

  1. SquareCabin 類別中,override buildingMaterial 屬性並指派屬性的值為 "Wood"
  2. capacity 執行相同動作,例如 6 位居民可以住在 SquareCabin
class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

完成的程式碼應如下所示:

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

若要測試程式碼,請在程式中建立 SquareCabin 的執行個體。

使用 SquareCabin

  1. DwellingSquareCabin 類別定義前,插入空白的 main() 函式。
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. main() 函式中,建立容納 6 位居民,名為 squareCabinSquareCabin 執行個體。新增建材、容量的輸出陳述式,以及 hasRoom() 函式。
fun main() {
    val squareCabin = SquareCabin(6)

    println("\nSquare Cabin\n============")
    println("Capacity: ${squareCabin.capacity}")
    println("Material: ${squareCabin.buildingMaterial}")
    println("Has room? ${squareCabin.hasRoom()}")
}

請注意,在 SquareCabin 類別中未定義 hasRoom() 函式,但在 Dwelling 類別中為已定義。因為 SquareCabinDwelling 類別的子類別,所以會無條件繼承 hasRoom() 函式。SquareCabin 的所有執行個體目前已可呼叫 hasRoom() 函式,如程式碼片段 squareCabin.hasRoom() 所示。

  1. 執行程式碼,程式碼會輸出以下內容:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

您已建立容納 6 位居民 (亦等於 capacity) 的 squareCabin,因此 hasRoom() 會傳回 false。使用較少的 residents 嘗試初始化 SquareCabin,並再次執行程式後,hasRoom() 會傳回 true

使用 with 簡化程式碼

println() 陳述式中,每次參考 squareCabin 的屬性或函式時,請注意您必須重複 squareCabin. 的原因。因為複製及貼上輸出陳述式時,可能會發生錯誤。

使用類別的特定執行個體,且必須存取該執行個體的多個屬性和函式時,可以使用 with 陳述式說明:「對這個執行個體物件執行下列所有作業」。以關鍵字 with 開頭,接著是括號中的執行個體名稱,後面加上大括號,包含您要執行的作業。

with (instanceName) {
    // all operations to do with instanceName
}
  1. main() 函式中,將輸出陳述式變更為使用 with
  2. 刪除輸出陳述式中的 squareCabin.
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. 再次執行程式碼,確認程式碼可正常執行,並顯示相同的輸出內容。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

以下是已完成的程式碼:

fun main() {
    val squareCabin = SquareCabin(6)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

建立 RoundHut 子類別

  1. SquareCabin 使用相同的方式,新增另一個子類別 (RoundHut) 至 Dwelling
  2. 覆寫 buildingMaterial 並指定為 "Straw" 的值。
  3. 覆寫 capacity 並設為 4。
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. main() 中,建立容納 3 位居民的 RoundHut 執行個體。
val roundHut = RoundHut(3)
  1. 加入下方的程式碼,即可輸出 roundHut 的資訊。
with(roundHut) {
    println("\nRound Hut\n=========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}
  1. 執行程式碼,整個程式的輸出內容應為:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

您目前的類別階層如下所示,其中 Dwelling 是根層級類別,而 SquareCabinRoundHutDwelling 的子類別。

c19084f4a83193a0.png

建立 RoundTower 子類別

這個類別階層最後一個類別是圓塔。圓塔就像是石頭建造、多個樓層的圓形小屋。所以您可以將 RoundTower 設為 RoundHut 的子類別。

  1. 建立 RoundTower 類別,即 RoundHut 的子類別。將 residents 參數新增至 RoundTower 的建構函式,然後將該參數傳遞至 RoundHut 父類別的建構函式。
  2. buildingMaterial 覆寫為 "Stone"
  3. capacity 設為 4
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. 執行這個程式碼後,您會看到錯誤訊息。
This type is final, so it cannot be inherited from

這個錯誤代表無法將 RoundHut 類別設為子類別 (或繼承該類別)。根據預設,在 Kotlin 中,類別為最終類別,且無法加入子類別。只能繼承 abstract 類別,或標示 open 關鍵字的類別。因此,您必須以 open 關鍵字標示 RoundHut 類別,才能繼承類別。

  1. 請在 RoundHut 宣告開頭加入 open 關鍵字。
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. main() 中,建立 roundTower 的執行個體,並輸出相關資訊。
 val roundTower = RoundTower(4)
with(roundTower) {
    println("\nRound Tower\n==========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}

以下是完整程式碼。

fun main() {
    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}

class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. 執行程式碼。程式碼應該可以正常執行,並產生下列輸出內容。
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Round Tower
==========
Material: Stone
Capacity: 4
Has room? false

新增多個樓層至 RoundTower

RoundHut (圓形茅屋) 是單層建築物。塔通常是多層 (樓層)。

想一下容量,塔的樓層越多,容量就越多。

您可以修改 RoundTower 為多個樓層,並根據樓層數調整容量。

  1. 更新 RoundTower 建構函式,並針對樓層數採用額外的整數參數 val floors。在 residents 後方置放參數。請注意,您不必傳遞參數至父項 RoundHut 建構函式,因為在 RoundTowerRoundHut 中定義的 floors 是沒有 floors
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. 執行程式碼。在 main() 方法中建立 roundTower 時發生錯誤,因為您未提供 floors 引數的數字。您可以新增遺漏的引數。

您也可以在 RoundTower 的類別定義中,新增 floors 的預設值,如下所示。接著,如果 floors 的值未傳遞至建構函式,您可以使用預設值來建立物件執行個體。

  1. 在程式碼中,在 floors 的宣告後方加上 = 2,即可指派預設值 2。
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    ...
}
  1. 執行程式碼。程式碼目前應該可以編譯,因為 RoundTower(4) 已可使用 2 個樓層的預設值,建立 RoundTower 物件執行個體。
  2. RoundTower 類別中,更新 capacity 即可乘以樓層數。
override val capacity = 4 * floors
  1. 執行程式碼,並留意 RoundTower 2 個層樓的容量目前為 8。

以下是已完成的程式碼。

fun main() {

    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}

class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors
}

5. 修改階層中的類別

計算樓層面積

在本練習中,您將瞭解在抽象類別中,如何宣告抽象函式,並在子類別實作函式。

所有居所都有樓層面積,但根據居所的形狀,計算方式會有所不同。

定義居所類別的 floorArea()

  1. 首先,新增 abstract floorArea() 函式至 Dwelling 類別。傳回 Double。Double 是資料類型 (例如 StringInt),並用於浮點數目,即包含小數點與其後小數部分的數字 (例如 5.8793)。
abstract fun floorArea(): Double

抽象類別中定義的所有抽象方法必須在其子類別中實作。執行程式碼前,您必須在子類別中實作 floorArea()

實作 SquareCabin 的 floorArea()

buildingMaterialcapacity 一樣,既然要實作父項類別定義的 abstract 函式,就必須使用 override 關鍵字。

  1. SquareCabin 類別中,以關鍵字 override 做為開頭,接著實際實作 floorArea() 函式,如下所示。
override fun floorArea(): Double {

}
  1. 傳回計算的樓層面積。長方形或正方形的面積是邊長乘以另一個邊長。函式主體是 return length * length
override fun floorArea(): Double {
    return length * length
}

長度不是類別中的變數,也與所有執行個體不同,所以您可以新增長度,做為 SquareCabin 類別的建構函式參數。

  1. 變更 SquareCabin 的類別定義,並新增 Double 類型的 length 參數。請宣告屬性為 val,因為建築物的長度不變。
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {

所以 Dwelling 和所有子類別都包含 residents,做為建構函式的引數。因為這是 Dwelling 建構函式的第一個引數,所以最佳做法是將該引數設為所有子類別建構函式的第一個引數,並以相同的順序在所有類別定義中放置引數。因此,請在 residents 參數後方插入新的 length 參數。

  1. main() 中更新 squareCabin 執行個體的建立作業。傳遞 50.0SquareCabin 建構函式,做為 length
val squareCabin = SquareCabin(6, 50.0)
  1. squareCabinwith 陳述式中,新增樓層面積的輸出陳述式。
println("Floor area: ${floorArea()}")

程式碼目前不執行的原因是,RoundHut 中也必須實作 floorArea()

實作 RoundHut 的 floorArea()

請以相同方式,實作 RoundHut 的樓層面積。RoundHut 也是 Dwelling 的直接子類別,所以您必須使用 override 關鍵字。

圓形居所的樓層面積是 PI * 半徑 * 半徑。

PI 是數學值,由數學程式庫定義。程式庫是程式外部定義的函式和數的集合,可供程式使用。若要使用程式庫的函式或值,您必須說明編譯器要使用的函式或值。方法是在程式匯入函式或值。若要在程式中使用 PI,請匯入 kotlin.math.PI

  1. 從 Kotlin 數學程式庫匯入 PI。在檔案頂端的 main() 前放置下列內容。
import kotlin.math.PI
  1. 實作 RoundHutfloorArea() 函式。
override fun floorArea(): Double {
    return PI * radius * radius
}

警告:如果您未匯入 kotlin.math.PI,就會收到錯誤訊息,因此必須先匯入這個程式庫才能使用。或者,您可以撰寫 PI 的完整版本,例如 kotlin.math.PI * radius * radius,則不需要匯入陳述式。

  1. 更新 RoundHut 建構函式,並傳入 radius
open class RoundHut(
   residents: Int,
   val radius: Double) : Dwelling(residents) {
  1. main() 中,傳遞 10.0radiusRoundHut 建構函式,即可更新 roundHut 的初始化作業。
val roundHut = RoundHut(3, 10.0)
  1. roundHutwith 陳述式中,加入輸出陳述式。
println("Floor area: ${floorArea()}")

實作 RoundTower 的 floorArea()

程式碼未執行,並因為下列錯誤失敗:

Error: No value passed for parameter 'radius'

RoundTower 中,若要程式順利編譯,其實不必實作 floorArea(),因為圓塔會繼承 RoundHut 的樓層面積,但為了使用相同的 radius 引數做為其父項 RoundHut,您必須更新 RoundTower 類別定義。

  1. 變更 RoundTower 的建構函式,讓其也接受 radius。在 residents 後方和 floors 前放置 radius。建議您使用結尾列出包含預設值的變數。請記得傳遞 radius 至父項類別建構函式。
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. 更新 main() 中的 roundTower 初始化作業。
val roundTower = RoundTower(4, 15.5)
  1. 然後加入呼叫 floorArea() 的輸出陳述式。
println("Floor area: ${floorArea()}")
  1. 您現在可以執行程式碼了!
  2. 請注意,RoundTower 的計算不正確,因為圓塔繼承 RoundHut,所以不會計算 floors 的數目。
  3. RoundTower 中,override floorArea() 提供不同的實作,讓面積可以乘以樓層數。請注意,您可以在抽象類別 (Dwelling) 中定義函式、在子類別 (RoundHut) 中實作函式,然後在子類別 (RoundTower) 的子類別中,再次覆寫該函式。這是兩個類別的最佳做法,您可以繼承需要的功能,並覆寫不必要的功能。
override fun floorArea(): Double {
    return PI * radius * radius * floors
}

這個程式碼可以使用,但 RoundHut 父項類別中,已有方法可以避免重複的程式碼。您可以從父類別 RoundHut 呼叫 floorArea() 函式,該函式會傳回 PI * radius * radius。然後再將此結果乘以 floors 的數目。

  1. RoundTower 中,更新 floorArea() 並使用 floorArea() 的父類別實作。使用 super 關鍵字呼叫父項中定義的函式。
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. 再次執行程式碼,然後 RoundTower 會輸出多個樓層的正確樓層空間。

以下是已完成的程式碼:

import kotlin.math.PI

fun main() {

    val squareCabin = SquareCabin(6, 50.0)
    val roundHut = RoundHut(3, 10.0)
    val roundTower = RoundTower(4, 15.5)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }
 }

abstract class Dwelling(private var residents: Int) {

    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
        return residents < capacity
}

    abstract fun floorArea(): Double
}

class SquareCabin(residents: Int,
    val length: Double) : Dwelling(residents) {

    override val buildingMaterial = "Wood"
    override val capacity = 6

    override fun floorArea(): Double {
       return length * length
    }
}

open class RoundHut(residents: Int,
    val radius: Double) : Dwelling(residents) {

    override val buildingMaterial = "Straw"
    override val capacity = 4

    override fun floorArea(): Double {
        return PI * radius * radius
    }
}

class RoundTower(residents: Int, radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors

    override fun floorArea(): Double {
        return super.floorArea() * floors
    }
}

輸出內容應如下所示:

Square Cabin
============
Capacity: 6
Material: Wood
Has room? false
Floor area: 2500.0

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true
Floor area: 314.1592653589793

Round Tower
==========
Material: Stone
Capacity: 8
Has room? true
Floor area: 1509.5352700498956

允許新居民取得房間

新增功能,使用 getRoom() 函式增加 1 位居民的數目,讓新居民取得房間。這個邏輯適用所有居所,所以您可以在 Dwelling 中實作這個函式,同時所有子類別和其子項都可使用這個函式。真棒!

注意:

  • 使用 if 陳述式,只在有容量剩餘時,才能新增居民。
  • 輸出結果訊息。
  • 您可以使用 residents++ 做為新增 1 至 residents 變數的函式 residents = residents + 1 簡寫。
  1. Dwelling 類別中實作 getRoom() 函式。
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. 新增一些輸出陳述式至 roundHutwith 陳述式區塊,並觀察搭配使用 getRoom()hasRoom() 的變化。
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()

這些輸出陳述式的輸出內容:

Has room? true
You got a room!
Has room? false
Sorry, at capacity and no rooms left.

詳情請參閱解決方案程式碼。

調整圓形居所的地毯

假設您需要知道 RoundHutRoundTower 的地毯側邊長度是多少。在 RoundHut 放置函式,供所有圓形居所使用。

2e328a198a82c793.png

  1. 首先,從 kotlin.math 程式庫匯入 sqrt() 函式。
import kotlin.math.sqrt
  1. RoundHut 類別中實作 calculateMaxCarpetLength() 函式。以下公式可以計算置於圓形居所的方形地毯長度:sqrt(2) * radius。詳情請見上圖。
fun calculateMaxCarpetLength(): Double {

    return sqrt(2.0) * radius
}

Double2.0 傳至數學函式 sqrt(2.0),因為函式的傳回類型為 Double,而非Integer

  1. RoundHutRoundTower 執行個體目前已可呼叫 calculateMaxCarpetLength() 方法。新增輸出陳述式至 main() 函式中的 roundHutroundTower
println("Carpet Length: ${calculateMaxCarpetLength()}")

詳情請參閱解決方案程式碼。

恭喜!您已建立包含屬性和函式的完整類別階層,並瞭解如何建立更實用的類別!

6. 解決方案程式碼

以下是本程式碼研究室的完整解決方案程式碼,包括相關註解。

/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/

import kotlin.math.PI
import kotlin.math.sqrt

fun main() {
   val squareCabin = SquareCabin(6, 50.0)
   val roundHut = RoundHut(3, 10.0)
   val roundTower = RoundTower(4, 15.5)

   with(squareCabin) {
       println("\nSquare Cabin\n============")
       println("Capacity: ${capacity}")
       println("Material: ${buildingMaterial}")
       println("Floor area: ${floorArea()}")
   }

   with(roundHut) {
       println("\nRound Hut\n=========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Carpet size: ${calculateMaxCarpetLength()}")
   }

   with(roundTower) {
       println("\nRound Tower\n==========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Carpet Length: ${calculateMaxCarpetLength()}")
   }
}

/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
   abstract val buildingMaterial: String
   abstract val capacity: Int

   /**
    * Calculates the floor area of the dwelling.
    * Implemented by subclasses where shape is determined.
    *
    * @return floor area
    */
   abstract fun floorArea(): Double

   /**
    * Checks whether there is room for another resident.
    *
    * @return true if room available, false otherwise
    */
   fun hasRoom(): Boolean {
       return residents < capacity
   }

   /**
    * Compares the capacity to the number of residents and
    * if capacity is larger than number of residents,
    * add resident by increasing the number of residents.
    * Print the result.
    */
   fun getRoom() {
       if (capacity > residents) {
           residents++
           println("You got a room!")
       } else {
           println("Sorry, at capacity and no rooms left.")
       }
   }

   }

/**
* A square cabin dwelling.
*
*  @param residents Current number of residents
*  @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
   override val buildingMaterial = "Wood"
   override val capacity = 6

   /**
    * Calculates floor area for a square dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return length * length
   }

}

/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
       residents: Int, val radius: Double) : Dwelling(residents) {

   override val buildingMaterial = "Straw"
   override val capacity = 4

   /**
    * Calculates floor area for a round dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return PI * radius * radius
   }

   /**
    *  Calculates the max length for a square carpet
    *  that fits the circular floor.
    *
    * @return length of square carpet
    */
    fun calculateMaxCarpetLength(): Double {
        return sqrt(2.0) * radius
    }

}

/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
       residents: Int,
       radius: Double,
       val floors: Int = 2) : RoundHut(residents, radius) {

   override val buildingMaterial = "Stone"

   // Capacity depends on the number of floors.
   override val capacity = floors * 4

   /**
    * Calculates the total floor area for a tower dwelling
    * with multiple stories.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return super.floorArea() * floors
   }
}

7. 匯總

在本程式碼研究室中,您瞭解如何:

  • 建立類別階層 (即類別的樹狀結構),以及子項會繼承父項類別的功能。子類別會繼承屬性和函式。
  • 建立 abstract 類別,其子類別會實作其餘的部分功能。所以 abstract 類別無法執行個體化。
  • 建立 abstract 類別的子類別。
  • 使用 override 關鍵字覆寫子類別的屬性和函式。
  • 使用 super 關鍵字參考父項類別的函式和屬性。
  • 建立類別 open,即可加入子類別。
  • 建立屬性 private,然後只能在類別中使用該屬性。
  • 使用 with 建構函式,多次呼叫相同的物件執行個體。
  • kotlin.math 程式庫匯入功能

8. 瞭解詳情