上一篇文章介绍了类对成员的声明方式与使用过程,从而初步了解了类的成员及其运用。不过早在《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接口的内部方法只能是抽象方法。

__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。

Kotlin入门(14)继承的那些事儿的更多相关文章

  1. Kotlin入门教程——目录索引

    Kotlin是谷歌官方认可的Android开发语言,Android Studio从3.0版本开始就内置了Kotlin,所以未来在App开发中Kotlin取代Java是大势所趋,就像当初Android ...

  2. Kotlin入门(32)网络接口访问

    手机上的资源毕竟有限,为了获取更丰富的信息,就得到辽阔的互联网大海上冲浪.对于App自身,也要经常与服务器交互,以便获取最新的数据显示到界面上.这个客户端与服务端之间的信息交互,基本使用HTTP协议进 ...

  3. 写给Android开发者的Kotlin入门

    写给Android开发者的Kotlin入门 转 https://www.jianshu.com/p/bb53cba6c8f4 Google在今年的IO大会上宣布,将Android开发的官方语言更换为K ...

  4. Kotlin入门(11)江湖绝技之特殊函数

    上一篇文章介绍了Kotlin对函数的输入参数所做的增强之处,其实函数这块Kotlin还有好些重大改进,集中体现在几类特殊函数,比如泛型函数.内联函数.扩展函数.尾递归函数.高阶函数等等,因此本篇文章就 ...

  5. Kotlin入门(15)独门秘笈之特殊类

    上一篇文章介绍了Kotlin的几种开放性修饰符,以及如何从基类派生出子类,其中提到了被abstract修饰的抽象类.除了与Java共有的抽象类,Kotlin还新增了好几种特殊类,这些特殊类分别适应不同 ...

  6. Kotlin入门第二课:集合操作

    测试项目Github地址: KotlinForJava 前文传送: Kotlin入门第一课:从对比Java开始 初次尝试用Kotlin实现Android项目 1. 介绍 作为Kotlin入门的第二课, ...

  7. js面向对象之继承那点事儿根本就不是事

    继承 说道这个继承,了解object-oriented的朋友都知道,大多oo语言都有两种,一种是接口继承(只继承方法签名):一种是实现继承(继承实际的方法) 奈何js中没有签名,因而只有实现继承,而且 ...

  8. Kotlin入门(28)Application单例化

    Application是Android的又一大组件,在App运行过程中,有且仅有一个Application对象贯穿应用的整个生命周期,所以适合在Application中保存应用运行时的全局变量.而开展 ...

  9. Kotlin入门(5)字符串及其格式化

    上一篇文章介绍了数组的声明和操作,包括字符串数组的用法.注意到Kotlin的字符串类也叫String,那么String在Java和Kotlin中的用法有哪些差异呢?这便是本文所要阐述的内容了. 首先要 ...

随机推荐

  1. Testing - 软件测试知识梳理 - 测试用例

    测试用例 是指对一项特定的软件产品进行测试任务的描述,体现测试方案.方法.技术和策略. 内容包括测试目标.测试环境.输入数据.测试步骤.预期结果.测试脚本等,并形成文档. 每个具体测试用例都将包括下列 ...

  2. Spring Boot日志集成实战

    Spring Boot日志框架 Spring Boot支持Java Util Logging,Log4j2,Lockback作为日志框架,如果你使用starters启动器,Spring Boot将使用 ...

  3. LeetCode--No.011 Container With Most Water

    11. Container With Most Water Total Accepted: 86363 Total Submissions: 244589 Difficulty: Medium Giv ...

  4. C# 获取媒体文件播放时长

    引用: Interop.Shell32.dll 方法: /// <summary> /// 获取媒体文件播放时长 /// </summary> /// <param na ...

  5. Nginx + Uswgi + Django的部署

    Nginx + Uswgi + Django的部署 待更新 https://code.ziqiangxuetang.com/django/django-static-files.html https: ...

  6. package.json文件中dependencies和devDependencies的区别

    在工作和学习中,我经常会用的npm 下载各种包,有时就会遇到各种npm 的形式,现在就捋一捋 首先要先了解   package.json文件中dependencies和devDependencies的 ...

  7. 用RIPv2实现网络区域的互通

    1.动态路由的分类: DV协议:距离矢量协议 距离矢量:路由器只能够知道邻居路由的信息 LS协议:链路状态协议 链路状态:路由器能够知道所在协议内的所有信息 RIP协议的全程是:路由信息协议(DV协议 ...

  8. ckeditor 在dwz里面使用

    在ckeditor的配置的过程中,所有的配置的地方都配置了,但是就是不显示编辑器(编辑器代码如下),很郁闷哦 1 <textarea id="editor1" name=&q ...

  9. 从零开始学 Web 之 JavaScript(五)面向对象

    大家好,这里是「 Daotin的梦呓 」从零开始学 Web 系列教程.此文首发于「 Daotin的梦呓 」公众号,欢迎大家订阅关注.在这里我会从 Web 前端零基础开始,一步步学习 Web 相关的知识 ...

  10. paperpass

    推荐大家一个靠谱的论文检测平台.重复的部分有详细出处以及具体修改意见,能直接在文章上做修改,全部改完一键下载就搞定了.怕麻烦的话,还能用它自带的降重功能.哦对了,他们现在正在做毕业季活动, 赠送很多免 ...