Kotlin入门(14)继承的那些事儿
上一篇文章介绍了类对成员的声明方式与使用过程,从而初步了解了类的成员及其运用。不过早在《Kotlin入门(12)类的概貌与构造》中,提到MainActivity继承自AppCompatActivity,而Kotlin对于类继承的写法是“class MainActivity : AppCompatActivity() {}”,这跟Java对比有明显差异,那么Kotlin究竟是如何定义基类并由基类派生出子类呢?为廓清这些迷雾,本篇文章就对类继承的相关用法进行深入探讨。
博文《Kotlin入门(13)类成员的众生相》在演示类成员时多次重写了WildAnimal类,这下你兴冲冲地准备按照MainActivity的继承方式,从WildAnimal派生出一个子类Tiger,写好构造函数的两个输入参数,补上基类的完整声明,敲了以下代码不禁窃喜这么快就大功告成了:
1 2 | 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进行演示,改写后的基类代码框架如下:
1 2 3 | 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类的类名、函数名、变量名加上修饰符,改写之后的基类代码是下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | //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 } } } |
好不容易鼓捣出来一个正儿八经的鸟儿基类,再来声明一个它的子类试试,例如鸭子是鸟类的一种,于是下面有了鸭子的类定义代码:
1 2 3 4 | //注意父类Bird已经在构造函数声明了属性,故而子类Duck无需重复声明属性 //也就是说,子类的构造函数,在输入参数前面不要再加val和var class Duck(name:String= "鸭子" , sex:Int = Bird.MALE) : Bird(name, sex) { } |
子类也可以定义新的成员属性和成员方法,或者重写被声明为open的父类方法。比方说性别名称“公”和“母”一般用于家禽,像公鸡、母鸡、公鸭、母鸭等等,指代野生鸟类的性别则通常使用“雄”和“雌”,所以定义野生鸟类的时候,就得重写获取性别名称的getSexName方法,把“公”和“母”的返回值改为“雄”和“雌”。方法重写之后,定义了鸵鸟的类代码如下所示:
1 2 3 4 5 6 7 8 9 10 | 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类代码示例如下:
1 2 3 4 5 6 7 8 | //子类的构造函数,原来的输入参数不用加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类代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 | 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类代码如下所示:
1 2 3 4 5 6 7 8 9 10 | 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类的该方法了,调用代码示例如下:
1 2 3 4 | //调用公鸡类的叫唤方法 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,下面是一个定义好的行为接口代码例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | //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类为例,重写接口方法之后的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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类声明的群鹅不但具备鸟类的基本功能,而且能飞、能游、能跑,活脱脱一只栩栩如生的大白鹅呀:
1 2 3 4 5 6 7 | 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接口的内部方法只能是抽象方法。
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· PostgreSQL 和 SQL Server 在统计信息维护中的关键差异
· C++代码改造为UTF-8编码问题的总结
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 【.NET】调用本地 Deepseek 模型