本文收錄于 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類允青,因?yàn)锳ny類中只有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) {//子類副硅,子類必須要實(shí)現(xiàn)父類中的一個構(gòu)造方法
}
有幾點(diǎn)需要注意:
- kotlin中的類默認(rèn)是final的,即是無法繼承的翅萤,這與java不同恐疲,java中默認(rèn)都是可繼承的。kotlin中所有的設(shè)計都是要顯示提供套么,其實(shí)這也正是kotlin的設(shè)計理念培己,只有在真正需要的時候才暴露。kotlin提供了open關(guān)鍵字用于顯示表明該類是可繼承的胚泌。
- 子類必須要實(shí)現(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){
}
在實(shí)際編碼中快耿,具體采用上面哪種寫法可以根據(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是合法的,因?yàn)楦割愒摲椒ㄊ褂昧薿pen修飾笔呀,表示可以被復(fù)寫
super.getName()
}
override fun getAge(){}//!!! 這是不合法的幢踏,編譯不通過!因?yàn)楦割愔械膅etAge()并沒有顯示指定為open
fun getAge(){}//!!! 這也是不合法的许师,編譯不通過房蝉!因?yàn)楦割愔幸呀?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ù)寫。屬性的繼承有幾點(diǎn)需要注意的灼捂,示例如下
//父類离例,該類設(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宫蛆,分析的重點(diǎn)就在這里。
class Student : Person() {
//首先看var變量
var age: Int = 20//!!!編譯不通過的猛,父類已經(jīng)存在該字段耀盗。
override var height: Int = 180//!!!編譯不通過,因?yàn)楦割愔袥]有顯示定義為open卦尊,故不能復(fù)寫叛拷。
override var address: String = "address"http://正確,因?yàn)楦割愔酗@示定義為了open
//下面是val變量
val name: String = "name"http://!!!編譯不通過岂却,父類已經(jīng)存在該字段忿薇。
override val email: String = "email"http://正確,因?yàn)楦割愔酗@示定義為了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")
}
//這里運(yùn)用了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")
}//注意,這里看著比較繞级零,但是實(shí)際完成功能就是打印基類的入?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í)斐蛇\(yùn)行時異常甚纲。
因此,在設(shè)計父類的時候朦前,一定要避免在構(gòu)造方法介杆、屬性初始化以及init塊中使用open類型的成員變量(因?yàn)檫@些晚些時候可能會被子類復(fù)寫)鹃操。
調(diào)用父類中的實(shí)現(xiàn)
kotlin同java一樣,子類要調(diào)用父類的實(shí)現(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中,只要父類中的實(shí)現(xiàn)(屬性或者方法)不是private的赴背,子類都可以通過super來調(diào)用父類的實(shí)現(xiàn)椰拒。
復(fù)寫規(guī)則
這里的復(fù)寫規(guī)則講的是,當(dāng)一個子類實(shí)現(xiàn)多個父實(shí)現(xiàn)的時候凰荚,會存在多個父實(shí)現(xiàn)含有相同實(shí)現(xiàn)的情形(如含有相同的方法簽名或者相同的屬性)燃观。注意,kotlin同java一樣便瑟,依然是單繼承體系缆毁,即一個子類一次只能繼承一個父類,這里所說的父實(shí)現(xiàn)是指到涂,子類可能會在繼承父類的同時實(shí)現(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")
}
}
//實(shí)現(xiàn)類C,繼承了A同時實(shí)現(xiàn)了B接口
class C : A(), B {//多個實(shí)現(xiàn)的寫法使用英文逗號(,)隔開
//注意這里聂儒,因?yàn)锳類中有方法m1,B接口中也有方法m1虎锚,所以子類就不知道該默認(rèn)實(shí)現(xiàn)哪個父實(shí)現(xiàn)中的方法。因此衩婚,在這種情形下窜护,kotlin會強(qiáng)制子類明確復(fù)寫該方法。如果子類還想調(diào)用父類的實(shí)現(xiàn)非春,那么可以通過super<父類型>這種方法來指定調(diào)用父類的實(shí)現(xiàn)柱徙,
override fun m1() {//該方法必須要復(fù)寫
super<A>.m1()//這里調(diào)用A類中m1的實(shí)現(xiàn),非強(qiáng)制奇昙,可選擇性調(diào)用
super<B>.m1()//調(diào)用B接口中m1的實(shí)現(xiàn)
}
}
上面代碼中护侮,由于m1存在實(shí)現(xiàn)沖突(兩個父實(shí)現(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()//抽象方法不能有任何實(shí)現(xiàn)伐脖,即不能有方法體{}
open fun m3() {//抽象類可以包含普通的方法實(shí)現(xiàn)
print("m3 in A")
}
}
//子類C,繼承抽象類A
class C : A() {
//子類必須要實(shí)現(xiàn)抽象類中的抽象方法乐设。普通方法則不強(qiáng)制實(shí)現(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)方法”的需求拒迅。從代碼也可以看出骚秦,包級別的方法不依附于任何類,也就是不屬于任何類璧微。但是假如有個方法需要在一個類中定義作箍,而我們確實(shí)又需要在不生成該類實(shí)例的情況下使用該方法,該怎么辦呢(如工廠方法模式)前硫?
針對這種情況胞得,kotlin提供了另一個實(shí)現(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類實(shí)例
}
}
實(shí)際上葱色,我們前面已經(jīng)多次用到伴隨對象了,比如程序的執(zhí)行入口Main類中main方法的實(shí)現(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>) {
}