上一篇文章介紹了類對成員的聲明方式與使用過程,從而初步瞭解了類的成員及其運用。不過早在《Kotlin入門(12)類的概貌與構造》中,提到MainActivity繼承自AppCompatActivity,而Kotlin對於類繼承的寫法是「class MainActivity : AppCompatActivity() {}」,這跟Java對比有明顯差別,那麼Kotlin到底是如何定義基類並由基類派生出子類呢?爲廓清這些迷霧,本篇文章就對類繼承的相關用法進行深刻探討。html
博文《Kotlin入門(13)類成員的衆生相》在演示類成員時屢次重寫了WildAnimal類,這下你興沖沖地準備按照MainActivity的繼承方式,從WildAnimal派生出一個子類Tiger,寫好構造函數的兩個輸入參數,補上基類的完整聲明,敲了如下代碼不由竊喜這麼快就大功告成了:java
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類的類名、函數名、變量名加上修飾符,改寫以後的基類代碼是下面這樣:app
//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:String var sexName:String init { 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 = 0 val FEMALE = 1 val UNKNOWN = -1 fun judgeSex(sexName:String):Int { var sex:Int = when (sexName) { "公","雄" -> MALE "母","雌" -> FEMALE else -> UNKNOWN } return sex } } }
好不容易鼓搗出來一個正兒八經的鳥兒基類,再來聲明一個它的子類試試,例如鴨子是鳥類的一種,因而下面有了鴨子的類定義代碼:框架
//注意父類Bird已經在構造函數聲明瞭屬性,故而子類Duck無需重複聲明屬性 //也就是說,子類的構造函數,在輸入參數前面不要再加val和var class Duck(name:String="鴨子", sex:Int = Bird.MALE) : Bird(name, sex) { }
子類也能夠定義新的成員屬性和成員方法,或者重寫被聲明爲open的父類方法。比方說性別名稱「公」和「母」通常用於家禽,像公雞、母雞、公鴨、母鴨等等,指代野生鳥類的性別則一般使用「雄」和「雌」,因此定義野生鳥類的時候,就得重寫獲取性別名稱的getSexName方法,把「公」和「母」的返回值改成「雄」和「雌」。方法重寫以後,定義了鴕鳥的類代碼以下所示:ide
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,但不能降級爲private override 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):String abstract fun callOut(times:Int):String }
接着從Chicken類派生出公雞類Cock,指定公雞的聲音爲「喔喔喔」,同時還要重寫callOut方法,明確公雞的叫喚行爲。具體的Cock類代碼以下所示:htm
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 -> 0 times>=10 -> 9 else -> 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 -> 0 times>=10 -> 9 else -> times } return "$sexName$name${voice}叫了${numberArray[count]}聲,原來它下蛋了呀。" } }
定義好了callOut方法,外部便可調用Cock類和Hen類的該方法了,調用代碼示例以下:blog
//調用公雞類的叫喚方法 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中定義接口須要注意如下幾點:
一、接口不能定義構造函數,不然編譯器會報錯「An interface may not have a constructor」;
二、接口的內部方法一般要被實現它的類進行重寫,因此這些方法默認爲抽象類型;
三、與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:String var 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所指的對象也能夠是interface return 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接口的內部方法只能是抽象方法。
__________________________________________________________________________
本文現已同步發佈到微信公衆號「老歐說安卓」,打開微信掃一掃下面的二維碼,或者直接搜索公衆號「老歐說安卓」添加關注,更快更方便地閱讀技術乾貨。