上一篇文章介紹了類對成員的聲明方式與使用過程,從而初步了解了類的成員及其運用。不過早在《Kotlin入門(12)類的概貌與構造》中,提到MainActivity繼承自AppCompatActivity,而Kotlin對于類繼承的寫法是“class MainActivity : AppCompatActivity() {}”,這跟Java對比有明顯差異,那么Kotlin究竟是如何定義基類并由基類派生出子類呢?為廓清這些迷霧,本篇文章就對類繼承的相關用法進行深入探討。
?
博文《Kotlin入門(13)類成員的眾生相》在演示類成員時多次重寫了WildAnimal類,這下你興沖沖地準備按照MainActivity的繼承方式,從WildAnimal派生出一個子類Tiger,寫好構造函數的兩個輸入參數,補上基類的完整聲明,敲了以下代碼不禁竊喜這么快就大功告成了:
class Tiger(name:String="老虎", sex:Int = 0) : WildAnimal(name, sex) {
}
誰料編譯器無情地蹦出錯誤提示“The type is final, so it cannot be inherited from”,意思是WildAnimal類是final類型,所以它不允許被繼承。原來Java默認每個類都能被繼承,除非加了關鍵字final表示終態,才不能被其它類繼承。Kotlin恰恰相反,它默認每個類都不能被繼承(相當于Java類被final修飾了),如果要讓某個類成為基類,則需把該類開放出來,也就是添加關鍵字open作為修飾。因此,接下來還是按照Kotlin的規矩辦事,重新寫個采取open修飾的基類,下面即以鳥類Bird進行演示,改寫后的基類代碼框架如下:
open class Bird (var name:String, val sex:Int = 0) {//此處暫時省略基類內部的成員屬性和方法
}
現在有了基類框架,還得往里面補充成員屬性和成員方法,然后給這些成員添加開放性修飾符。就像大家在Java和C++世界中熟知的幾個關鍵字,包括public、protected、private,分別表示公開、只對子類開放、私有。那么Kotlin體系參照Java世界也給出了四個開放性修飾符,按開放程度從高到低分別是:
public : 對所有人開放。Kotlin的類、函數、變量不加開放性修飾符的話,默認就是public類型。
internal : 只對本模塊內部開放,這是Kotlin新增的關鍵字。對于App開發來說,本模塊便是指App自身。
protected : 只對自己和子類開放。
private : 只對自己開放,即私有。
注意到這幾個修飾符與open一樣都加在類和函數前面,并且都包含“開放”的意思,乍看過去還真有點撲朔迷離,到底open跟四個開放性修飾符是什么關系?其實也不復雜,open不控制某個對象的訪問權限,只決定該對象能否繁衍開來,說白了,就是公告這個家伙有沒有資格生兒育女。只有頭戴open帽子的類,才允許作為基類派生出子類來;而頭戴open帽子的函數,表示它允許在子類中進行重寫。
至于那四個開放性修飾符,則是用來限定允許訪問某對象的外部范圍,通俗地說,就是哪里的男人可以娶這個美女。頭戴public的,表示全世界的男人都能娶她;頭戴internal的,表示本國的男人可以娶她;頭戴protected的,表示本單位以及下屬單位的男人可以娶她;頭戴private的,表示肥水不流外人田,只有本單位的帥哥才能娶這個美女噢。
因為private的限制太嚴厲了,只對自己開放,甚至都不允許子類染指,所以它跟關鍵字open勢同水火。open表示這個對象可以被繼承,或者可以被重載,然而private卻堅決斬斷該對象與其子類的任何關系,因此二者不能并存。倘若在代碼中強行給某個方法同時加上open和private,編譯器只能無奈地報錯“Modifier 'open' is incompatible with 'private'”,意思是open與private不兼容。
按照以上的開放性相關說明,接下來分別給Bird類的類名、函數名、變量名加上修飾符,改寫之后的基類代碼是下面這樣:
//Kotlin的類默認是不能繼承的(即final類型),如果需要繼承某類,則該父類應當聲明為open類型。
//否則編譯器會報錯“The type is final, so it cannot be inherited from”。
open class Bird (var name:String, val sex:Int = MALE) {//變量、方法、類默認都是public,所以一般都把public省略掉了//public var sexName:Stringvar sexName:Stringinit {sexName = getSexName(sex)}//私有的方法既不能被外部訪問,也不能被子類繼承,因此open與private不能共存//否則編譯器會報錯:Modifier 'open' is incompatible with 'private'//open private fun getSexName(sex:Int):String {open protected fun getSexName(sex:Int):String {return if(sex==MALE) "公" else "母"}fun getDesc(tag:String):String {return "歡迎來到$tag:這只${name}是${sexName}的。"}companion object BirdStatic{val MALE = 0val FEMALE = 1val UNKNOWN = -1fun judgeSex(sexName:String):Int {var sex:Int = when (sexName) {"公","雄" -> MALE"母","雌" -> FEMALEelse -> UNKNOWN}return sex}}
}
好不容易鼓搗出來一個正兒八經的鳥兒基類,再來聲明一個它的子類試試,例如鴨子是鳥類的一種,于是下面有了鴨子的類定義代碼:
//注意父類Bird已經在構造函數聲明了屬性,故而子類Duck無需重復聲明屬性
//也就是說,子類的構造函數,在輸入參數前面不要再加val和var
class Duck(name:String="鴨子", sex:Int = Bird.MALE) : Bird(name, sex) {
}
子類也可以定義新的成員屬性和成員方法,或者重寫被聲明為open的父類方法。比方說性別名稱“公”和“母”一般用于家禽,像公雞、母雞、公鴨、母鴨等等,指代野生鳥類的性別則通常使用“雄”和“雌”,所以定義野生鳥類的時候,就得重寫獲取性別名稱的getSexName方法,把“公”和“母”的返回值改為“雄”和“雌”。方法重寫之后,定義了鴕鳥的類代碼如下所示:
class Ostrich(name:String="鴕鳥", sex:Int = Bird.MALE) : Bird(name, sex) {//繼承protected的方法,標準寫法是“override protected”//override protected fun getSexName(sex:Int):String {//不過protected的方法繼承過來默認就是protected,所以也可直接省略protected//override fun getSexName(sex:Int):String {//protected的方法繼承之后允許將可見性升級為public,但不能降級為privateoverride public fun getSexName(sex:Int):String {return if(sex==MALE) "雄" else "雌"}
}
除了上面講的普通類繼承,Kotlin也存在與Java類似的抽象類,抽象類之所以存在,是因為其內部擁有被abstract修飾的抽象方法。抽象方法沒有具體的函數體,故而外部無法直接聲明抽象類的實例;只有在子類繼承之時重寫抽象方法,該子類方可正常聲明對象實例。舉個例子,雞屬于鳥類,可公雞和母雞的叫聲是不一樣的,公雞是“喔喔喔”地叫,而母雞是“咯咯咯”地叫;所以雞這個類的叫喚方法callOut,發出什么聲音并不確定,只能先聲明為抽象方法,連帶著雞類Chicken也變成抽象類了。根據上述的抽象類方案,定義好的Chicken類代碼示例如下:
//子類的構造函數,原來的輸入參數不用加var和val,新增的輸入參數必須加var或者val。
//因為抽象類不能直接使用,所以構造函數不必給默認參數賦值。
abstract class Chicken(name:String, sex:Int, var voice:String) : Bird(name, sex) {val numberArray:Array<String> = arrayOf("一","二","三","四","五","六","七","八","九","十");//抽象方法必須在子類進行重寫,所以可以省略關鍵字open,因為abstract方法默認就是open類型//open abstract fun callOut(times:Int):Stringabstract fun callOut(times:Int):String
}
接著從Chicken類派生出公雞類Cock,指定公雞的聲音為“喔喔喔”,同時還要重寫callOut方法,明確公雞的叫喚行為。具體的Cock類代碼如下所示:
class Cock(name:String="雞", sex:Int = Bird.MALE, voice:String="喔喔喔") : Chicken(name, sex, voice) {override fun callOut(times: Int): String {var count = when {//when語句判斷大于和小于時,要把完整的判斷條件寫到每個分支中times<=0 -> 0times>=10 -> 9else -> times}return "$sexName$name${voice}叫了${numberArray[count]}聲,原來它在報曉呀。"}
}
同樣派生而來的母雞類Hen,也需指定母雞的聲音“咯咯咯”,并重寫callOut叫喚方法,具體的Hen類代碼如下所示:
class Hen(name:String="雞", sex:Int = Bird.FEMALE, voice:String="咯咯咯") : Chicken(name, sex, voice) {override fun callOut(times: Int): String {var count = when {times<=0 -> 0times>=10 -> 9else -> times}return "$sexName$name${voice}叫了${numberArray[count]}聲,原來它下蛋了呀。"}
}
定義好了callOut方法,外部即可調用Cock類和Hen類的該方法了,調用代碼示例如下:
//調用公雞類的叫喚方法tv_class_inherit.text = Cock().callOut(count++%10)//調用母雞類的叫喚方法tv_class_inherit.text = Hen().callOut(count++%10)
既然提到了抽象類,就不得不提接口interface。Kotlin的接口與Java一樣是為了間接實現多重繼承,由于直接繼承多個類可能存在方法沖突等問題,因此Kotlin在編譯階段就不允許某個類同時繼承多個基類,否則會報錯“Only one class may appear in a supertype list”。于是乎,通過接口定義幾個抽象方法,然后在實現該接口的具體類中重寫這幾個方法,從而間接實現C++多重繼承的功能。
在Kotlin中定義接口需要注意以下幾點:
1、接口不能定義構造函數,否則編譯器會報錯“An interface may not have a constructor”;
2、接口的內部方法通常要被實現它的類進行重寫,所以這些方法默認為抽象類型;
3、與Java不同的是,Kotlin允許在接口內部實現某個方法,而Java接口的所有內部方法都必須是抽象方法;
Android開發最常見的接口是控件的點擊監聽器View.OnClickListener,其內部定義了控件的點擊動作onClick,類似的還有長按監聽器View.OnLongClickListener、選擇監聽器CompoundButton.OnCheckedChangeListener等等,它們無一例外都定義了某種行為的事件處理過程。對于本文的鳥類例子而言,也可通過一個接口定義鳥兒的常見動作行為,譬如鳥兒除了叫喚動作,還有飛翔、游泳、奔跑等等動作,有的鳥類擅長飛翔(如大雁、老鷹),有的鳥類擅長游泳(如鴛鴦、鸕鶿),有的鳥類擅長奔跑(如鴕鳥、鴯鹋)。因此針對鳥類的飛翔、游泳、奔跑等動作,即可聲明Behavior接口,在該接口中定義幾個行為方法如fly、swim、run,下面是一個定義好的行為接口代碼例子:
//Kotlin與Java一樣不允許多重繼承,即不能同時繼承兩個類(及以上類),
//否則編譯器報錯“Only one class may appear in a supertype list”,
//所以仍然需要接口interface來間接實現多重繼承的功能。
//接口不能帶構造函數(那樣就變成一個類了),否則編譯器報錯“An interface may not have a constructor”
//interface Behavior(val action:String) {
interface Behavior {//接口內部的方法默認就是抽象的,所以不加abstract也可以,當然open也可以不加open abstract fun fly():String//比如下面這個swim方法就沒加關鍵字abstract,也無需在此處實現方法fun swim():String//Kotlin的接口與Java的區別在于,Kotlin接口內部允許實現方法,//此時該方法不是抽象方法,就不能加上abstract,//不過該方法依然是open類型,接口內部的所有方法都默認是open類型fun run():String {return "大多數鳥兒跑得并不像樣,只有鴕鳥、鴯鹋等少數鳥類才擅長奔跑。"}//Kotlin的接口允許聲明抽象屬性,實現該接口的類必須重載該屬性,//與接口內部方法一樣,抽象屬性前面的open和abstract也可省略掉//open abstract var skilledSports:Stringvar skilledSports:String
}
那么其他類實現Behavior接口時,跟類繼承一樣把接口名稱放在冒號后面,也就是說,Java的extends和implement這兩個關鍵字在Kotlin中都被冒號取代了。然后就像重寫抽象類的抽象方法一樣,重寫該接口的抽象方法,以鵝的Goose類為例,重寫接口方法之后的代碼如下所示:
class Goose(name:String="鵝", sex:Int = Bird.MALE) : Bird(name, sex), Behavior {override fun fly():String {return "鵝能飛一點點,但飛不高,也飛不遠。"}override fun swim():String {return "鵝,鵝,鵝,曲項向天歌。白毛浮綠水,紅掌撥清波。"}//因為接口已經實現了run方法,所以此處可以不用實現該方法,當然你要實現它也行。override fun run():String {//super用來調用父類的屬性或方法,由于Kotlin的接口允許實現方法,因此super所指的對象也可以是interfacereturn super.run()}//重載了來自接口的抽象屬性override var skilledSports:String = "游泳"
}
這下大功告成,Goose類聲明的群鵝不但具備鳥類的基本功能,而且能飛、能游、能跑,活脫脫一只栩栩如生的大白鵝呀:
btn_interface_behavior.setOnClickListener {tv_class_inherit.text = when (count++%3) {0 -> Goose().fly()1 -> Goose().swim()else -> Goose().run()}}
總結一下,Kotlin的類繼承與Java相比有所不同,首先Kotlin的類默認不可被繼承,如需繼承則要添加open聲明;而Java的類默認是允許被繼承的,只有添加final聲明才表示不能被繼承。其次,Kotlin除了常規的三個開放性修飾符public、protected、private,另外增加了修飾符internal表示只對本模塊開放。再次,Java的類繼承關鍵字extends,以及接口實現關鍵字implement,在Kotlin中都被冒號所取代。最后,Kotlin允許在接口內部實現某個方法,而Java接口的內部方法只能是抽象方法。
__________________________________________________________________________
本文現已同步發布到微信公眾號“老歐說安卓”,打開微信掃一掃下面的二維碼,或者直接搜索公眾號“老歐說安卓”添加關注,更快更方便地閱讀技術干貨。
?