Kotlin入门(14)继承的那些事儿
上一篇文章介绍了类对成员的声明方式与使用过程,从而初步了解了类的成员及其运用。不过早在《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: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方法,把“公”和“母”的返回值改为“雄”和“雌”。方法重写之后,定义了鸵鸟的类代码如下所示:
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类代码如下所示:
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类的该方法了,调用代码示例如下:
//调用公鸡类的叫唤方法 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: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接口的内部方法只能是抽象方法。
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。