Kotlin教學 | 從零開始學Kotlin | Kotlin入門 | CH7: 繼承(Inheritance)

繼承

繼承允許一個子類別獲得父類別的成員,並可以使用覆蓋來重新實作獲得(繼承)的成員。

主要會有二個動作:

  • 定義父類別。
  • 定義子類別繼承父類別,如果需要重新實作父類別成員,則覆蓋父類別成員。

基本結構

open class ParentClass(param1: Type1, param2: Type2) {
    // 可以被覆蓋的成員變量(屬性)
    open var mutableProperty: Type1 = initialValue
    open val immutableProperty: Type2 = initialValue

    // 可以被覆蓋的成員函數
    open fun functionName() {
        // 函數體
    }
}

class ChildClass(param1: Type1, param2: Type2) : ParentClass(param1, param2) {
    // 覆蓋的成員變量(屬性)
    override var mutableProperty: Type1 = initialValue
    override val immutableProperty: Type2 = initialValue

    // 覆蓋的成員函數
    override fun functionName() {
        // 覆蓋的函數體
    }
}
  • open:在 Kotlin 中,類別和它的成員預設是不可以被繼承或覆蓋的(final)。
    若希望一個類別可以被繼承或它的成員可以被覆蓋,需要使用 open 關鍵字進行標記。
  • ParentClass:這是父類別的名稱,提供了基礎屬性和函數給子類別繼承。
  • ChildClass:這是子類別的名稱,通過 : 符號和後續的ParentClass父類別名稱來表達繼承關係。
    子類別可以覆蓋父類別中被標記為 open 的屬性和函數。
  • override:當子類別需要覆蓋父類別中的成員時(無論是屬性還是函數),必須使用 override 關鍵字來明確表示。
  • 根類別:Kotlin 中所有類別都繼承自 Any 類別(根類別)。
    如果一個類別沒有聲明任何繼承,那麼它默認繼承自 Any
    Any 提供了三個方法:equals()hashCode()toString(),因此這些方法在所有 Kotlin 類別中都是可用的。

Example

Shape 有一個可以被繼承的方法 draw
Circle 繼承 Shape 和覆蓋draw

open class Shape {
    open fun draw() { /*...*/ }
}

class Circle() : Shape() { // Circle 繼承 Shape
    override fun draw() { /*...*/ } // 覆蓋 draw 方法
}

初始化和構造函數

  • 當子類別繼承父類別時,子類別的主構造函數會呼叫父類別的構造函數,傳遞必要的參數以初始化父類別。
  • 子類別可以有自己的初始化塊和次構造函數,次構造函數需要使用 super 關鍵字呼叫父類別的構造函數,或使用 this 關鍵字間接呼叫到父類別的構造函數。
  • 原則就是,子類別的構造函數(主要和次要)必須最終都可以呼叫到父類別的構造函數。

Example

子類別有主構造函數

如果子類別有主構造函數,則必須在主構造函數中呼叫父類別的構造函數(主要或次要)。

open class Base(p: Int) { // 父類別主構造函數
    /* ... */
}

class Derived(p: Int) : Base(p) // 子類別呼叫父類別主構造函數
open class Base {
    constructor(p: Int) { /* ... */ } // 父類別次構造函數
}

class Derived(p: Int) : Base(p) // 子類別呼叫父類別次構造函數

子類別只有次構造函數

如果子類別只有次構造函數,則次構造函數需要使用 super 關鍵字呼叫父類別的構造函數,或使用 this 關鍵字間接呼叫到父類別的構造函數。

open class Base() { // Base主構造函數
	constructor(p: Int) : this() // Base次構造函數
}

class Derived : Base { // Derived沒有主構造函數
    constructor() : super() // 呼叫Base主構造函數
    constructor(p: Int) : super(p) // 呼叫Base次構造函數
    constructor(p: Int, q: Int) : this() // 呼叫Derived次構造函數
}

覆蓋

覆蓋允許子類別修改繼承自父類別的行為。
通過覆蓋,開發者可以自定義或增強繼承來的功能,使得子類別能夠在保持類別的成員(接口)一致的同時,提供不同於父類別的行為實作。

覆蓋是實作多型 (polymorphism) 的一種方式,多型(Polymorphism)是物件導向編程中的一個核心概念,指的是不同的子類別可以繼承相同的父類別,所有的子類別在保持統一的父類別成員(接口)的同時,又可以採用自己的方式來實做這些成員。

Example

在主構造函數中也可以使用 override 關鍵字作為屬性宣告的一部分。

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle(override val vertexCount: Int = 4) : Shape() // 始終有 4 個頂點

防止再覆蓋

一個被標記為 override的成員是可以再次被覆蓋的。
如果不希望override的成員再次被覆蓋,可以使用 final 修飾符。

open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
    final override val vertexCount = 4
}

父類別和子類別初始化順序

Kotlin在創建子類別物件前要先創建父類別物件。
因此,當我們創建一個子類別的實例時,父類別的初始化邏輯會在子類別的初始化邏輯執行之前完成。

但是,如果當父類別的構造函數被執行時,子類別中宣告或覆蓋的屬性尚未被初始化。
在父類別的初始化邏輯中直接或間接地使用這些屬性可能導致錯誤或非預期的行為。
因此,在設計父類別時,應避免在構造函數、屬性初始化器或初始化塊(init)中使用 open 成員。

下面程式碼展示當父類別的構造函數被執行時,子類別中覆蓋的屬性size尚未被初始化,導致父類別得到的size值和之後子類別初始化的size值是不一樣的。

open class Base(val name: String) {

    init { println("Initializing a base class") }

    open val size: Int = 
        name.length.also { println("Initializing size in the base class: $it") }
}

class Derived(
    name: String,
    val lastName: String,
) : Base(name.also { println("Argument for the base class: $it") }) {

    init { println("Initializing a derived class") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in the derived class: $it") }
}
fun main() {
	Derived("AB", "CDE")
}

調用父類別的方法或屬性

在 Kotlin 中,子類別可以使用 super 關鍵字來調用其父類別的函數或屬性。
讓子類別在修改父類別的行為時,還能夠利用父類別提供的功能。

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw() // 呼叫父類別 Rectangle 的 draw()
        println("Filling the rectangle")
    }

    val fillColor = super.borderColor // 訪問父類別的 borderColor
}

在內部類別中訪問外部類別的父類別

當需要在內部類別中訪問外部類別的父類別時,可以通過在 super@加上外部類別名稱的方式來實作。

class FilledRectangle: Rectangle() {
    override fun draw() { /*...*/ }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            super@FilledRectangle.draw() // 呼叫父類別 Rectangle 的 draw()
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // 訪問父類別 Rectangle 的 borderColor
        }
    }
}

覆蓋規則

子類別可以同時繼承一個父類別和多個介面
如果父類別和介面,都有相同被繼承的成員(屬性或方法)時,Kotlin無法知道子類別要繼承誰的成員。
因此,當有多個相同被繼承的成員時,子類別必須覆蓋該成員和重新實作。

可以使用 super 關鍵字加上角括號和父類別或介面名稱,指名調用父類別還是介面的成員。

下面範例,Square 繼承 Rectangle 類別和 Polygon 介面,但兩者都有 draw()
因此 Square 必須覆蓋 draw() 並重新實作。
實作內容是呼叫 Rectangledraw()Polygondraw()

open class Rectangle {
    open fun draw() { /* */ }
}

interface Polygon {
    fun draw() { /* 介面也有相同的 draw() 成員函數,介面的函數默認是'open' */ }
}

class Square() : Rectangle(), Polygon {
    // 必須覆蓋 draw()
    override fun draw() {
        super<Rectangle>.draw() // 調用 Rectangle 的 draw()
        super<Polygon>.draw() // 調用 Polygon 的 draw()
    }
}

Reference

https://kotlinlang.org/docs/inheritance.html

發佈留言