本文收錄于 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)造方法
}
有幾點需要注意:
- kotlin中的類默認(rèn)是final的,即是無法繼承的,這與java不同,java中默認(rèn)都是可繼承的。kotlin中所有的設(shè)計都是要顯示提供,其實這也正是kotlin的設(shè)計理念,只有在真正需要的時候才暴露。kotlin提供了open關(guān)鍵字用于顯示表明該類是可繼承的。
- 子類必須要實現(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>) {
}