kotlin入門潛修之類和對象篇—繼承

本文收錄于 kotlin入門潛修專題系列,歡迎學(xué)習(xí)交流。

創(chuàng)作不易,如有轉(zhuǎn)載,還請備注。

繼承

面向?qū)ο蟮娜蠡豪^承、多態(tài)與封裝。這三個特性構(gòu)成了絢麗多彩的編程世界,也衍生出了諸多優(yōu)雅的設(shè)計。本篇文章將會解析kotlin中的繼承機(jī)制。

眾所周知,java中所有的類都會默認(rèn)繼承java.lang.Object類,同樣,kotlin中所有的類也默認(rèn)繼承了一個叫做Any的類,其作用同java的Object類,是kotlin里面所有類的基類。

需要注意的是,Any類雖然同java中的Object類一樣作為所有類的基類存在,但是Any類并不等同于java的Object類,因為Any類中只有equals、hasCode、toString三個方法,而java中的Object類還有諸如getClass、notifyAll、wait、clone等方法,所以二者并不是一個類。

kotlin中的繼承寫法也和java完全不一樣了,kotlin中不再有extends、implements關(guān)鍵字,取而代之的是冒號“ : ”,其定義如下:

open class Person constructor(name: String) {//基類,注意有open關(guān)鍵字修飾
}
class Student(name: String) : Person(name) {//子類,子類必須要實現(xiàn)父類中的一個構(gòu)造方法
}

有幾點需要注意:

  1. kotlin中的類默認(rèn)是final的,即是無法繼承的,這與java不同,java中默認(rèn)都是可繼承的。kotlin中所有的設(shè)計都是要顯示提供,其實這也正是kotlin的設(shè)計理念,只有在真正需要的時候才暴露。kotlin提供了open關(guān)鍵字用于顯示表明該類是可繼承的。
  2. 子類必須要實現(xiàn)父類中的一個構(gòu)造方法??梢酝ㄟ^子類的主構(gòu)造方法去初始化父類構(gòu)造方法,也可以通過第二構(gòu)造方法初始化父類的構(gòu)造方法。上面的例子就是通過主構(gòu)造方法初始化了父類。第二構(gòu)造方法初始化示例如下:
//父類People,注意,這里提供了一個主構(gòu)造方法和一個第二構(gòu)造方法
open class People constructor(name: String) {
    public constructor(name: String, age: Int) : this(name)
}
//下面是幾種不同的初始化父類的寫法
//1. 通過第二構(gòu)造方法初始化,這里調(diào)用了父類People的主構(gòu)造方法
class Teacher : People {
    constructor() : super("張三")
}
//2. 通過第二構(gòu)造方法初始化,這里調(diào)用了父類People的第二構(gòu)造方法
class Teacher : People {
    constructor() : super("張三", 10)
}
//3.通過主構(gòu)造方法初始化,這里調(diào)用了父類People的主構(gòu)造方法
class Teacher(name: String) : People (name){
}
//4.通過主構(gòu)造方法初始化,這里調(diào)用了父類People的第二構(gòu)造方法
class Teacher(name: String) : People (name, 20){
}

在實際編碼中,具體采用上面哪種寫法可以根據(jù)場景自行選擇。主要能夠保證初始化父類的任意構(gòu)造方法即可。

復(fù)寫方法(Overriding Methods)

kotlin中方法的復(fù)寫和類的設(shè)計理念一樣(類必須顯示定義為open才能被繼承),必須要顯示指定該方法可以復(fù)寫,子類才能進(jìn)行復(fù)寫(當(dāng)然前提是父類也必須定義為可繼承的,即要open修飾),其顯示指定的關(guān)鍵字依然是open。示例如下:

//父類,open修飾,表示可繼承
open class Person {
    fun getAge(){}//注意這里沒有open關(guān)鍵字
    open fun getName(){}//這里有open關(guān)鍵字
}
class Student() : Person() {
    override fun getName() {//這里override是合法的,因為父類該方法使用了open修飾,表示可以被復(fù)寫
        super.getName()
    }
    override fun getAge(){}//!!! 這是不合法的,編譯不通過!因為父類中的getAge()并沒有顯示指定為open
    fun getAge(){}//!!! 這也是不合法的,編譯不通過!因為父類中已經(jīng)存在getAge(),只能override。在這個例子中即使override也是不合法的,上面已經(jīng)闡述。
}

一個方法一旦被標(biāo)記為open方法,那么該方法就一直能被override(即其子類的子類的子類...等等都可以復(fù)寫),那么如果子類不想再讓其子類override方法怎么辦?比如上個例子中,Person中的getName是可被override的,所以子類Student可以通過override fun getName來復(fù)寫,但是現(xiàn)在Student不在期望其子類再override getName方法,該怎么辦?很簡單,在其方法前加final關(guān)鍵字即可:

open  class Student() : Person() {
    final override fun getName() {//注意這里加了final關(guān)鍵字,表示其子類不再能復(fù)寫該方法。
        super.getName()
    }
}

復(fù)寫屬性(overriding properties)

復(fù)寫屬性和復(fù)寫方法一樣,要用open顯示標(biāo)明可復(fù)寫。屬性的繼承有幾點需要注意的,示例如下

//父類,該類設(shè)置為了可繼承,即open修飾
open class Person {
    var age : Int = 20
    var height: Int = 170
    open var address : String = "address"
    val name : String = "name"
    open val email : String = "email"
    open val phoneNum : Int = 1234567
    open var score: Int = 80
    open val sex : String get() {return "男"}
}
//子類,繼承Person,分析的重點就在這里。
class Student : Person() {
    //首先看var變量
    var age: Int = 20//!!!編譯不通過,父類已經(jīng)存在該字段。
    override var height: Int = 180//!!!編譯不通過,因為父類中沒有顯示定義為open,故不能復(fù)寫。
    override var address: String = "address"http://正確,因為父類中顯示定義為了open
    //下面是val變量
    val name: String = "name"http://!!!編譯不通過,父類已經(jīng)存在該字段。
    override val email: String = "email"http://正確,因為父類中顯示定義為了open
    override var phoneNum : Int = 1234567//正確,注意,這里父類中的phoneNum是val不可變的,但這里復(fù)寫為了var可變的,kotlin是允許這么做的。
    override val score: Int = 80//!!!編譯錯誤,注意,這里父類中的score是var可變的,而這里復(fù)寫為了val不可變的,kotlin中是不允許這么做的。
    override val sex: String get() {//正確,這里只是演示了屬性變量另一種初始化方法,即使用get方法。
        return "男"
    }
}

上面基本分析了復(fù)寫屬性的各種情況,唯一需要注意的是父類中的val是可以在子類中被復(fù)寫為var的,反之則不行。這是為什么?

是這樣的,kotlin中的val屬性都默認(rèn)定義了一個getter方法,子類復(fù)寫為var的時候?qū)嶋H上是為該變量額外增加了一個setter方法,所以可以這么做。

此外,kotlin也可以在主構(gòu)造方法中復(fù)寫屬性,如下所示:

open class Person constructor(open val name: String) {
}
//注意,子類在主構(gòu)造方法中復(fù)寫了name屬性
open class Student(override val name: String) : Person(name) {
}

派生類的初始化順序

所謂派生類即是繼承父類的子類。那么派生類的執(zhí)行順序是怎么樣的?先看下面一個例子:

//父類
open class Person(name: String) {
    init {
        println("initializing person")
    }

//這里運用了let方法,會在后續(xù)文章中分析
    open val nameLength: Int = name.length.let {
        println("initializing name length in person:".plus(it))
        it
    }
}
//子類
class Student(name: String, lastName: String) : Person(name.let { println("argument for person $it")
            it }) {
    init {
        println("initializing student")
    }//注意,這里看著比較繞,但是實際完成功能就是打印基類的入?yún)?
    override val nameLength: Int = lastName.length.let {
        (super.nameLength + it).let {
            println("initializing name length in student:".plus(it))
            it
        }
    }
}
   //程序執(zhí)行入口
    @JvmStatic fun main(args: Array<String>) {
            var student = Student("name", "lastName")//生成student對象
     }

上面代碼執(zhí)行main方法后,會打印一下日志:

argument for person name
initializing person
initializing name length in person:4
initializing student
initializing name length in student:12

通過日志打印可以看出,kotlin會首先初始化父類,父類先執(zhí)行構(gòu)造方法,然后按編碼順序先后執(zhí)行init塊、屬性初始化等,接著會執(zhí)行子類構(gòu)造方法、init塊、屬性初始化等。

由此可知,在父類執(zhí)行構(gòu)造方法的時候,子類的屬性或者復(fù)寫父類的屬性都還沒有初始化,所以父類中一定不能使用這些屬性,否則會造成未知的錯誤,甚至?xí)斐蛇\行時異常。

因此,在設(shè)計父類的時候,一定要避免在構(gòu)造方法、屬性初始化以及init塊中使用open類型的成員變量(因為這些晚些時候可能會被子類復(fù)寫)。

調(diào)用父類中的實現(xiàn)

kotlin同java一樣,子類要調(diào)用父類的實現(xiàn)可以通過super關(guān)鍵字完成,示例如下:

//父類
open class Person() {
    open fun printSex() {
        println("默認(rèn)性別:男")
    }
    var defaultName = ""
    open val age = 20
}
//子類
class Student() : Person() {
    override fun printSex() {//復(fù)寫父類printSex方法
        super.printSex()//這里通過super調(diào)用父類中方法
        println("the student age: 18")
    }
   fun printName(){//子類自定義打印姓名的方法
        println(super.defaultName)//這里直接調(diào)用了父類中的非open屬性。
    }
    override val age: Int
        get() = super.age + 2//這里通過super調(diào)用父類中的open屬性
}

kotlin中,只要父類中的實現(xiàn)(屬性或者方法)不是private的,子類都可以通過super來調(diào)用父類的實現(xiàn)。

復(fù)寫規(guī)則

這里的復(fù)寫規(guī)則講的是,當(dāng)一個子類實現(xiàn)多個父實現(xiàn)的時候,會存在多個父實現(xiàn)含有相同實現(xiàn)的情形(如含有相同的方法簽名或者相同的屬性)。注意,kotlin同java一樣,依然是單繼承體系,即一個子類一次只能繼承一個父類,這里所說的父實現(xiàn)是指,子類可能會在繼承父類的同時實現(xiàn)了一個或者多個接口。具體示例如下:

//父類A,有m1和m2兩個方法
open class A {
    open fun m1() {
        print("m1 in A")
    }

    open fun m2() {
        print("m2 in A")
    }
}
//接口B,有m1和m3兩個方法,注意m1方法和A中的簽名一樣。
interface B {//kotlin中接口的寫法,使用關(guān)鍵字interface修飾
    fun m1() {//接口中的方法默認(rèn)都是open的,所以不需要使用open修飾
        print("m1 in B")
    }

    fun m3() {
        print("m3 in B")
    }
}
//實現(xiàn)類C,繼承了A同時實現(xiàn)了B接口
class C : A(), B {//多個實現(xiàn)的寫法使用英文逗號(,)隔開
        //注意這里,因為A類中有方法m1,B接口中也有方法m1,所以子類就不知道該默認(rèn)實現(xiàn)哪個父實現(xiàn)中的方法。因此,在這種情形下,kotlin會強(qiáng)制子類明確復(fù)寫該方法。如果子類還想調(diào)用父類的實現(xiàn),那么可以通過super<父類型>這種方法來指定調(diào)用父類的實現(xiàn),
        override fun m1() {//該方法必須要復(fù)寫
        super<A>.m1()//這里調(diào)用A類中m1的實現(xiàn),非強(qiáng)制,可選擇性調(diào)用
        super<B>.m1()//調(diào)用B接口中m1的實現(xiàn)
    }
}

上面代碼中,由于m1存在實現(xiàn)沖突(兩個父實現(xiàn)都有該方法),所以子類必須要復(fù)寫該方法,而m2、m3不存在沖突,故kotlin不強(qiáng)制復(fù)寫。

抽象類

kotlin中的抽象類同java一樣,都是使用abstract關(guān)鍵字來修飾。kotlin中的抽象類,默認(rèn)都是open的,所以不需要再顯示使用open關(guān)鍵字進(jìn)行修飾。如果一個類的任意一個成員被定義為abstract,那么該類必須要定義為抽象類。

示例如下:

abstract class A {//抽象類使用abstract修飾
    abstract fun m1()//抽象方法不能有任何實現(xiàn),即不能有方法體{}
    open fun m3() {//抽象類可以包含普通的方法實現(xiàn)
        print("m3 in A")
    }
}
//子類C,繼承抽象類A
class C : A() {
   //子類必須要實現(xiàn)抽象類中的抽象方法。普通方法則不強(qiáng)制實現(xiàn)。
    override fun m1() {
    }
}

伴隨對象

伴隨對象是kotlin中特有的存在。kotlin不像java、c#,它沒有static方法,而是推薦使用包級別(package-level)的方法替代,示例如下:

package com.test//com.test包
fun staticM1(){//直接定義了一個staticM1方法,注意這里并沒有定義任何類
    println("staticM1")
}
//在Main類中調(diào)用該包級別方法
import com.test.staticM1//導(dǎo)入了staticM1方法
class Main {
    companion object {//這個是個伴隨對象,下面會分析
        @JvmStatic fun main(args: Array<String>) {
            staticM1()//這里調(diào)用了staticM1,使用方法如同java中的static,沒有生成任何類對象
        }

    }
}

上面的寫法即是包級別的方法,大部分都可以滿足要使用“靜態(tài)方法”的需求。從代碼也可以看出,包級別的方法不依附于任何類,也就是不屬于任何類。但是假如有個方法需要在一個類中定義,而我們確實又需要在不生成該類實例的情況下使用該方法,該怎么辦呢(如工廠方法模式)?

針對這種情況,kotlin提供了另一個實現(xiàn)機(jī)制:伴隨對象。有了伴隨對象,就可以想調(diào)用靜態(tài)方法一樣使用了,如下所示:

class A {
    companion object {//伴隨對象的寫法,兩個關(guān)鍵字companion object
        fun m1() {//這里定義了一個m1方法,注意下面B類中的調(diào)用方式
            println("method m1 in A's companion object")
        }
    }
}

class B {
    fun test() {
        A.m1()//注意這里,通過A類名調(diào)用了m1方法,而沒有生成A類實例
    }
}

實際上,我們前面已經(jīng)多次用到伴隨對象了,比如程序的執(zhí)行入口Main類中main方法的實現(xiàn)。我們都知道java中的執(zhí)行入口是靜態(tài)方法,那么kotlin中的執(zhí)行入口該怎么寫呢?示例如下:

class Main {
    companion object {//伴隨對象
        @JvmStatic fun main(args: Array<String>) {//main方法執(zhí)行入口
        }
    }
}

當(dāng)然,也可以提供包級別的main方法,如下所示:

class Main {
//作為對比,這里暫時注釋掉了伴隨對象
//    companion object {
//        @JvmStatic fun main(args: Array<String>) {
//
//        }
//
//    }
}
//這里提供了package-level的main入口方法,作用同上面注釋掉的伴隨對象寫法。
fun main(args: Array<String>) {
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Kotlin的類和接口與Java的類和接口是有一定的區(qū)別的。Kotlin的接口是可以包含屬性聲明。Kotlin默認(rèn)...
    程自舟閱讀 10,520評論 0 11
  • 前言 人生苦多,快來 Kotlin ,快速學(xué)習(xí)Kotlin! 什么是Kotlin? Kotlin 是種靜態(tài)類型編程...
    任半生囂狂閱讀 26,699評論 9 118
  • 寫在開頭:本人打算開始寫一個Kotlin系列的教程,一是使自己記憶和理解的更加深刻,二是可以分享給同樣想學(xué)習(xí)Kot...
    胡奚冰閱讀 1,524評論 5 11
  • 面向?qū)ο缶幊蹋∣OP) 在前面的章節(jié)中,我們學(xué)習(xí)了Kotlin的語言基礎(chǔ)知識、類型系統(tǒng)、集合類以及泛型相關(guān)的知識。...
    Tenderness4閱讀 4,621評論 1 6
  • 對于回顧這段歷史。 用戰(zhàn)爭,屠殺,血腥,只會引來更多的仇恨。這是經(jīng)歷過這段歷史的所有人都想避免的。 美麗人生,最悲...
    JabinW閱讀 266評論 0 0

友情鏈接更多精彩內(nèi)容