上一篇文章介绍了Kotlin的几种开放性修饰符,以及如何从基类派生出子类,其中提到了被abstract修饰的抽象类。除了与Java共有的抽象类,Kotlin还新增了好几种特殊类,这些特殊类分别适应不同的使用场景,极大地方便了开发者的编码工作,下面就来看看Kotlin究竟提供了哪些独门秘笈。
嵌套类
一个类可以在单独的代码文件中定义,也可以在另一个类内部定义,后一种情况叫做嵌套类,意即A类嵌套在B类之中。乍看过去,这个嵌套类的定义似乎与Java的嵌套类是一样的,但其实有所差别。Java的嵌套类允许访问外部类的成员,而Kotlin的嵌套类不允许访问外部类的成员。倘若Kotlin的嵌套类内部强行访问外部类的成员,则编译器会报错“Unresolved reference: ***”,意思是找不到这个东西。下面是Kotlin定义嵌套类的代码例子:
class Tree(var treeName:String) {
//在类内部再定义一个类,这个新类称作嵌套类
class Flower (var flowerName:String) {
fun getName():String {
return "这是一朵$flowerName"
//普通的嵌套类不能访问外部类的成员如treeName
//否则编译器报错“Unresolved reference: ***”
//return "这是${treeName}上的一朵$flowerName"
}
}
}
调用嵌套类时,得在嵌套类的类名前面添加外部类的类名,相当于把这个嵌套类作为外部类的静态对象使用。嵌套类的调用代码如下所示:
btn_class_nest.setOnClickListener {
//使用嵌套类时,只能引用外部类的类名,不能调用外部类的构造函数
val peachBlossom = Tree.Flower("桃花");
tv_class_secret.text = peachBlossom.getName()
}
内部类
既然Kotlin限制了嵌套类不能访问外部类的成员,那还有什么办法可以实现此功能呢?针对该问题,Kotlin另外增加了关键字inner表示内部,把inner加在嵌套类的class前面,于是嵌套类华丽丽转变为了内部类,这个内部类比起嵌套类的好处,便是能够访问外部类的成员。所以,Kotlin的内部类就相当于Java的嵌套类,而Kotlin的嵌套类则是加了访问限制的内部类。按照前面演示嵌套类的树木类Tree,也给它补充内部类的定义,代码如下所示:
class Tree(var treeName:String) {
//在类内部再定义一个类,这个新类称作嵌套类
class Flower (var flowerName:String) {
fun getName():String {
return "这是一朵$flowerName"
//普通的嵌套类不能访问外部类的成员如treeName
//否则编译器报错“Unresolved reference: ***”
//return "这是${treeName}上的一朵$flowerName"
}
}
//嵌套类加上了inner前缀,就成为了内部类
inner class Fruit (var fruitName:String) {
fun getName():String {
//只有声明为内部类(添加了关键字inner),才能访问外部类的成员
return "这是${treeName}长出来的$fruitName"
}
}
}
调用内部类时,要先实例化外部类,再通过外部类的实例调用内部类的构造函数,也就是把内部类作为外部类的一个成员对象来使用,这与成员属性、成员方法的调用方法类似。内部类的调用代码如下所示:
btn_class_inner.setOnClickListener {
//使用内部类时,必须调用外部类的构造函数,否则编译器会报错
val peach = Tree("桃树").Fruit("桃花");
tv_class_secret.text = peach.getName()
}
枚举类
Java有一种枚举类型,它采用关键字enum来表达,其内部定义了一系列名称,通过有意义的名字比0/1/2这些数字能更有效地表达语义。下面是个Java定义枚举类型的代码例子:
enum Season { SPRING,SUMMER,AUTUMN,WINTER }
上面的枚举类型定义代码,看起来仿佛是一种新的数据类型,特别像枚举数组。可是枚举类型实际上是一种类,开发者在代码中创建enum类型时,编译器会自动生成一个对应的类,并且该类继承自java.lang.Enum。因此,Kotlin摒弃了“枚举类型”那种模糊不清的说法,转而采取“枚举类”这种正本清源的提法。具体到编码上,则将enum作为关键字class的修饰符,使之名正言顺地成为一个类——枚举类。按此思路将前面Java的枚举类型Season改写为Kotlin的枚举类,改写后的枚举类代码如下所示:
enum class SeasonType {
SPRING,SUMMER,AUTUMN,WINTER
}
枚举类内部的枚举变量,除了可以直接拿来赋值之外,还可以通过枚举值的几个属性获得对应的信息,例如ordinal属性用于获取该枚举值的序号,name属性用于获取该枚举值的名称。枚举变量本质上还是该类的一个实例,所以如果枚举类存在构造函数的话,枚举变量也必须调用对应的构造函数。这样做的好处是,每个枚举值不但携带唯一的名称,还可以拥有更加个性化的特征描述。比如下面的枚举类SeasonName代码,通过构造函数能够给枚举值赋予更加丰富的含义:
enum class SeasonName (val seasonName:String) {
SPRING("春天"),
SUMMER("夏天"),
AUTUMN("秋天"),
WINTER("冬天")
}
下面的代码演示了如何分别使用两个枚举类SeasonType和SeasonName:
btn_class_enum.setOnClickListener {
if (count%2 == 0) {
//ordinal表示枚举类型的序号,name表示枚举类型的名称
tv_class_secret.text = when (count++%4) {
SeasonType.SPRING.ordinal -> SeasonType.SPRING.name
SeasonType.SUMMER.ordinal -> SeasonType.SUMMER.name
SeasonType.AUTUMN.ordinal -> SeasonType.AUTUMN.name
SeasonType.WINTER.ordinal -> SeasonType.WINTER.name
else -> "未知"
}
} else {
tv_class_secret.text = when (count++%4) {
//使用自定义属性seasonName表示更个性化的描述
SeasonName.SPRING.ordinal -> SeasonName.SPRING.seasonName
SeasonName.SUMMER.ordinal -> SeasonName.SUMMER.seasonName
SeasonName.AUTUMN.ordinal -> SeasonName.AUTUMN.seasonName
SeasonName.WINTER.ordinal -> SeasonName.WINTER.seasonName
else -> "未知"
//枚举类的构造函数是给枚举类型使用的,外部不能直接调用枚举类的构造函数
//else -> SeasonName("未知").name
}
}
}
密封类
前面演示外部代码判断枚举值的时候,when语句末尾例行公事加了else分支。可是枚举类SeasonType内部一共只有四个枚举变量,when语句有四个分支就行了,最后的else分支纯粹是多此一举。出现此种情况的缘故是,when语句不晓得SeasonType只有四种枚举值,因此以防万一必须要有else分支,除非编译器认为现有的几个分支已经足够。
为解决枚举值判断的多余分支问题,Kotlin提出了“密封类”的概念,密封类就像是一种更加严格的枚举类,它内部有且仅有自身的实例对象,所以是一个有限的自身实例集合。或者说,密封类采用了嵌套类的手段,它的嵌套类全部由自身派生而来,仿佛一个家谱明明白白列出来某人有长子、次子、三子、幺子。定义密封类时使用关键字sealed标记,具体的密封类定义代码如下所示:
sealed class SeasonSealed {
//密封类内部的每个嵌套类都必须继承该类
class Spring (var name:String) : SeasonSealed()
class Summer (var name:String) : SeasonSealed()
class Autumn (var name:String) : SeasonSealed()
class Winter (var name:String) : SeasonSealed()
}
有了密封类,通过when语句便无需指定else分支了,下面是判断密封类对象的代码例子:
btn_class_sealed.setOnClickListener {
var season = when (count++%4) {
0 -> SeasonSealed.Spring("春天")
1 -> SeasonSealed.Summer("夏天")
2 -> SeasonSealed.Autumn("秋天")
else -> SeasonSealed.Winter("冬天")
}
//密封类是一种严格的枚举类,它的值是一个有限的集合。
//密封类确保条件分支覆盖了所有的枚举类型,因此不再需要else分支。
tv_class_secret.text = when (season) {
is SeasonSealed.Spring -> season.name
is SeasonSealed.Summer -> season.name
is SeasonSealed.Autumn -> season.name
is SeasonSealed.Winter -> season.name
}
}
数据类
在Android开发中,免不了经常定义一些存放数据的实体类,比如用户信息、商品信息等等,每逢定义实体类之时,开发者基本要手工完成以下编码工作:
1、定义实体类的每个字段,以及对字段进行初始赋值的构造函数;
2、定义每个字段的get/set方法;
3、在判断两个数据对象是否相等时,通常每个字段都比较一遍;
4、在复制数据对象时,如果想修改某几个字段的值,得再补充对应数量的赋值语句;
5、在调试程序时,为获知数据对象里保存的字段值,得手工把每个字段值都打印出来;
如此折腾一番,仅仅是定义一个实体类,开发者就必须完成这些繁琐的任务。然而这些任务其实毫无技术含量可言,如果每天都在周而复始地敲实体类的相关编码,毫无疑问跟工地上的搬砖民工差不多,活生生把程序员弄成一个拼时间拼体力的职业。有鉴于此,Kotlin再次不负众望推出了名为“数据类”的特殊类,直接戳中程序员事多、腰酸、睡眠少的痛点,极大程度上将程序员从无涯苦海中拯救出来。
数据类说神秘也不神秘,它的类定义代码极其简单,只要开发者在class前面增加关键字“data”,并声明入参完整的构造函数,即可无缝实现以下功能:
1、自动声明与构造入参同名的属性字段;
2、自动实现每个属性字段的get/set方法;
3、自动提供equals方法,用于比较两个数据对象是否相等;
4、自动提供copy方法,允许完整复制某个数据对象,也可在复制后单独修改某几个字段的值;
5、自动提供toString方法,用于打印数据对象中保存的所有字段值;
功能如此强大的数据类,犹如葵花宝典,让你功力倍增。见识了数据类的深厚功力,再来看看它的类代码是怎么定义的:
//数据类必须有主构造函数,且至少有一个输入参数,
//并且要声明与输入参数同名的属性,即输入参数前面添加关键字val或者var,
//数据类不能是基类也不能是子类,不能是抽象类,也不能是内部类,更不能是密封类。
data class Plant(var name:String, var stem:String, var leaf:String, var flower:String, var fruit:String, var seed:String) {
}
想不到吧,原来数据类的实现代码竟然如此简单,当真是此时无招胜有招。当然,为了达到这个代码精简的效果,数据类也得遵循几个规则,或者说是约束条件,毕竟不以规矩不成方圆,正如类定义代码所注释的那样:
1、数据类必须有主构造函数,且至少有一个输入参数,因为它的属性字段要跟输入参数一一对应,如果没有属性字段,这个数据类保存不了数据也就失去存在的意义了;
2、主构造函数的输入参数前面必须添加关键字val或者var,这保证每个入参都会自动声明同名的属性字段;
3、数据类有自己的一套行事规则,所以它只能是个独立的类,不能是其他类型的类,否则不同规则之间会产生矛盾;
现在利用上面定义的数据类——植物类Plant,演示看看外部如何操作数据类,具体调用代码如下所示:
var lotus = Plant("莲", "莲藕", "莲叶", "莲花", "莲蓬", "莲子")
//数据类的copy方法不带参数,表示复制一模一样的对象
var lotus2 = lotus.copy()
btn_class_data.setOnClickListener {
lotus2 = when (count++%2) {
//copy方法带参数,表示指定参数另外赋值
0 -> lotus.copy(flower="荷花")
else -> lotus.copy(flower="莲花")
}
//数据类自带equals方法,用于判断两个对象是否一样
var result = if (lotus2.equals(lotus)) "相等" else "不等"
tv_class_secret.text = "两个植物的比较结果是${result}\n" +
"第一个植物的描述是${lotus.toString()}\n" +
"第二个植物的描述是${lotus2.toString()}"
}
模板类
在前面的文章《Kotlin入门(11)江湖绝技之特殊函数》中,提到了泛型函数,当时把泛型函数作为全局函数定义,从而在别的地方也能调用它。那么如果某个泛型函数在类内部定义,即变成了这个类的成员方法,又该如何定义它呢?这个问题在Java中是通过模板类(也叫做泛型类)来解决的,例如常见的容器类ArrayList、HashMap均是模板类,Android开发中的异步任务AsyncTask也是模板类。
模板类的应用如此广泛,Kotlin自然而然保留了它,并且写法与Java类似,一样在类名后面补充形如“<T>”或者“<A, B>”的表达式,表示这里的类型待定,要等创建类实例时再确定具体的变量类型。待定的类型可以有一个,如ArrayList;可以有两个,如HashMap;也可以有三个或者更多,如AsyncTask。举个例子,森林里有一条小河,小河的长度可能以数字形式输入(包括Int、Long、Float、Double),也可能以字符串形式输入(String类型)。如果输入的是数字长度,则长度单位采取“m”;如果输入的是字符串长度,则长度单位采取“米”。按照以上需求编写名为River的模板类,具体的类定义代码如下:
//在类名后面添加“<T>”,表示这是一个模板类
class River<T> (var name:String, var length:T) {
fun getInfo():String {
var unit:String = when (length) {
is String -> "米"
//Int、Long、Float、Double都是数字类型Number
is Number -> "m"
else -> ""
}
return "${name}的长度是$length$unit。"
}
}
外部调用模板类构造函数的时候,要在类名后面补充“<参数类型>”,从而动态指定实际的参数类型。不过正如声明变量那样,如果编译器能够根据初始值判断该变量的类型,就无需显式指定该变量的类型;模板类也存在类似的偷懒写法,如果编译器根据输入参数就能知晓参数类型,则调用模板类的构造函数也不必显式指定参数类型。以下是外部使用模板类的代码例子:
btn_class_generic.setOnClickListener {
var river = when (count++%4) {
//模板类(泛型类)声明对象时,要在模板类的类名后面加上“<参数类型>”
0 -> River<Int>("小溪", 100)
//如果编译器根据输入参数就能知晓参数类型,也可直接省略“<参数类型>”
1 -> River("瀑布", 99.9f)
//当然保守起见,新手最好按规矩添加“<参数类型>”
2 -> River<Double>("山涧", 50.5)
//如果你已经是老手了,怎么方便怎么来,Kotlin的设计初衷就是偷懒
else -> River("大河", "一千")
}
tv_class_secret.text = river.getInfo()
}
总结一下,本文介绍了Kotlin的六种特殊函数,首先嵌套类和内部类都定义在某个外部类的内部,区别在于能否访问外部类的成员;其次枚举类和密封类都提供了有序的枚举值集合,区别在于密封类的定义更加严格;再次是帮助开发者摆脱搬砖命运的数据类;最后是解决未定参数类型的模板类(也叫泛型类)。
点此查看Kotlin入门教程的完整目录
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
![](https://i-blog.csdnimg.cn/blog_migrate/c9f61a85bb8e090ceef701c0d30ab15b.jpeg)