Kotlin研發第六彈——類與繼承

類與繼承

Kotlin 中使用關鍵字 class 聲明類

class Invoice { ... }

類聲明由類名、類頭(指定其類型參數、主構造函數等)以及由花括號包圍的類體構成。類頭與類體都是可選的; 如果一個類沒有類體,可以省略花括號。

class Empty

構造函數

在 Kotlin 中的一個類可以有一個主構造函數以及一個或多個次構造函數。主構造函數是類頭的一部分:它跟在類名(與可選的類型參數)後。

class Person constructor(firstName: String) { ... }

如果主構造函數沒有任何註解或者可見性修飾符,可以省略這個 constructor 關鍵字。

class Person(firstName: String) { ... }

主構造函數不能包含任何的代碼。初始化的代碼可以放到以 init 關鍵字作爲前綴的**初始化塊(initializer blocks)**中。

在實例初始化期間,初始化塊按照它們出現在類體中的順序執行,與屬性初始化器交織在一起:

class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints ${name}")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

Target platform: JVMRunning on kotlin v. 1.3.21

請注意,主構造的參數可以在初始化塊中使用。它們也可以在類體內聲明的屬性初始化器中使用:

class Customer(name: String) {
    val customerKey = name.toUpperCase()
}

事實上,聲明屬性以及從主構造函數初始化屬性,Kotlin 有簡潔的語法:

class Person(val firstName: String, val lastName: String, var age: Int) { …… }

與普通屬性一樣,主構造函數中聲明的屬性可以是可變的(var)或只讀的(val)。

如果構造函數有註解或可見性修飾符,這個 constructor 關鍵字是必需的,並且這些修飾符在它前面:

class Customer public @Inject constructor(name: String) { …… }

更多詳情,參見可見性修飾符

次構造函數

類也可以聲明前綴有 constructor次構造函數

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

如果類有一個主構造函數,每個次構造函數需要委託給主構造函數, 可以直接委託或者通過別的次構造函數間接委託。委託到同一個類的另一個構造函數用 this 關鍵字即可:

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

請注意,初始化塊中的代碼實際上會成爲主構造函數的一部分。委託給主構造函數會作爲次構造函數的第一條語句,因此所有初始化塊中的代碼都會在次構造函數體之前執行。即使該類沒有主構造函數,這種委託仍會隱式發生,並且仍會執行初始化塊:

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor")
    }
}

Target platform: JVMRunning on kotlin v. 1.3.21

如果一個非抽象類沒有聲明任何(主或次)構造函數,它會有一個生成的不帶參數的主構造函數。構造函數的可見性是 public。如果你不希望你的類有一個公有構造函數,你需要聲明一個帶有非默認可見性的空的主構造函數:

class DontCreateMe private constructor () { ... }

注意:在 JVM 上,如果主構造函數的所有的參數都有默認值,編譯器會生成 一個額外的無參構造函數,它將使用默認值。這使得 Kotlin 更易於使用像 Jackson 或者 JPA 這樣的通過無參構造函數創建類的實例的庫。

class Customer(val customerName: String = "")

創建類的實例

要創建一個類的實例,我們就像普通函數一樣調用構造函數:

val invoice = Invoice()

val customer = Customer("Joe Smith")

注意 Kotlin 並沒有 new 關鍵字。

創建嵌套類、內部類與匿名內部類的類實例在嵌套類中有述。

類成員

類可以包含:

繼承

在 Kotlin 中所有類都有一個共同的超類 Any,這對於沒有超類型聲明的類是默認超類:

class Example // 從 Any 隱式繼承

注意:Any 並不是 java.lang.Object;尤其是,它除了 equals()hashCode()toString() 外沒有任何成員。 更多細節請查閱Java互操作性部分。

要聲明一個顯式的超類型,我們把類型放到類頭的冒號之後:

open class Base(p: Int)

class Derived(p: Int) : Base(p)

如果派生類有一個主構造函數,其基類型可以(並且必須) 用基類的主構造函數參數就地初始化。

如果類沒有主構造函數,那麼每個次構造函數必須使用 super 關鍵字初始化其基類型,或委託給另一個構造函數做到這一點。 注意,在這種情況下,不同的次構造函數可以調用基類型的不同的構造函數:

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

覆蓋方法

我們之前提到過,Kotlin 力求清晰顯式。與 Java 不同,Kotlin 對於可覆蓋的成員(我們稱之爲開放)以及覆蓋後的成員需要顯式修飾符:

open class Base {
    open fun v() { ... }
    fun nv() { ... }
}
class Derived() : Base() {
    override fun v() { ... }
}

Derived.v() 函數上必須加上 override 修飾符。如果沒寫,編譯器將會報錯。 如果函數沒有標註 openBase.nv(),那麼子類中不允許定義相同簽名的函數, 不論加不加 override。將 open 修飾符添加到 final 類(即沒有 open 的類)的成員上不起作用。

標記爲 override 的成員本身是開放的,也就是說,它可以在子類中覆蓋。如果你想禁止再次覆蓋,使用 final 關鍵字:

open class AnotherDerived() : Base() {
    final override fun v() { ... }
}

覆蓋屬性

屬性覆蓋與方法覆蓋類似;在超類中聲明然後在派生類中重新聲明的屬性必須以 override 開頭,並且它們必須具有兼容的類型。每個聲明的屬性可以由具有初始化器的屬性或者具有 getter 方法的屬性覆蓋。

open class Foo {
    open val x: Int get() { …… }
}

class Bar1 : Foo() {
    override val x: Int = ……
}

你也可以用一個 var 屬性覆蓋一個 val 屬性,但反之則不行。這是允許的,因爲一個 val 屬性本質上聲明瞭一個 getter 方法,而將其覆蓋爲 var 只是在子類中額外聲明一個 setter 方法。

請注意,你可以在主構造函數中使用 override 關鍵字作爲屬性聲明的一部分。

interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

派生類初始化順序

在構造派生類的新實例的過程中,第一步完成其基類的初始化(在之前只有對基類構造函數參數的求值),因此發生在派生類的初始化邏輯運行之前。

open class Base(val name: String) {

    init { println("Initializing Base") }

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

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

    init { println("Initializing Derived") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}

Target platform: JVMRunning on kotlin v. 1.3.21

這意味着,基類構造函數執行時,派生類中聲明或覆蓋的屬性都還沒有初始化。如果在基類初始化邏輯中(直接或通過另一個覆蓋的 open 成員的實現間接)使用了任何一個這種屬性,那麼都可能導致不正確的行爲或運行時故障。設計一個基類時,應該避免在構造函數、屬性初始化器以及 init 塊中使用 open 成員。

調用超類實現

派生類中的代碼可以使用 super 關鍵字調用其超類的函數與屬性訪問器的實現:

open class Foo {
    open fun f() { println("Foo.f()") }
    open val x: Int get() = 1
}

class Bar : Foo() {
    override fun f() { 
        super.f()
        println("Bar.f()") 
    }
    
    override val x: Int get() = super.x + 1
}

在一個內部類中訪問外部類的超類,可以通過由外部類名限定的 super 關鍵字來實現:super@Outer

class Bar : Foo() {
    override fun f() { /* …… */ }
    override val x: Int get() = 0
    
    inner class Baz {
        fun g() {
            super@Bar.f() // 調用 Foo 實現的 f()
            println(super@Bar.x) // 使用 Foo 實現的 x 的 getter
        }
    }
}

覆蓋規則

在 Kotlin 中,實現繼承由下述規則規定:如果一個類從它的直接超類繼承相同成員的多個實現, 它必須覆蓋這個成員並提供其自己的實現(也許用繼承來的其中之一)。 爲了表示採用從哪個超類型繼承的實現,我們使用由尖括號中超類型名限定的 super,如 super<Base>

open class A {
    open fun f() { print("A") }
    fun a() { print("a") }
}

interface B {
    fun f() { print("B") } // 接口成員默認就是“open”的
    fun b() { print("b") }
}

class C() : A(), B {
    // 編譯器要求覆蓋 f():
    override fun f() {
        super<A>.f() // 調用 A.f()
        super<B>.f() // 調用 B.f()
  }
}

同時繼承 AB 沒問題,並且 a()b() 也沒問題因爲 C 只繼承了每個函數的一個實現。 但是 f()C 繼承了兩個實現,所以我們必須C 中覆蓋 f() 並且提供我們自己的實現來消除歧義。

抽象類

類以及其中的某些成員可以聲明爲 abstract。 抽象成員在本類中可以不用實現。 需要注意的是,我們並不需要用 open 標註一個抽象類或者函數——因爲這不言而喻。

我們可以用一個抽象成員覆蓋一個非抽象的開放成員

open class Base {
    open fun f() {}
}

abstract class Derived : Base() {
    override abstract fun f()
}

伴生對象

與 Java 或 C# 不同,在 Kotlin 中類沒有靜態方法。在大多數情況下,它建議簡單地使用包級函數。

如果你需要寫一個可以無需用一個類的實例來調用、但需要訪問類內部的函數(例如,工廠方法),你可以把它寫成該類內對象聲明中的一員。

更具體地講,如果在你的類內聲明瞭一個伴生對象, 你就可以使用像在 Java/C# 中調用靜態方法相同的語法來調用其成員,只使用類名作爲限定符。

跳轉上一章 Kotlin研發第五彈——控制流:if、when、for、while

跳轉下一章 Kotlin研發第七彈——屬性與字段

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章