Kotlin入门(15)独门秘笈之特殊类
上一篇文章介绍了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方法,用于打印数据对象中保存的所有字段值;
功能如此强大的数据类,犹如步枪界的AK47,持有该款自动步枪的战士无疑战斗力倍增。见识了数据类的深厚功力,再来看看它的类代码是怎么定义的:
//数据类必须有主构造函数,且至少有一个输入参数, //并且要声明与输入参数同名的属性,即输入参数前面添加关键字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的六种特殊函数,首先嵌套类和内部类都定义在某个外部类的内部,区别在于能否访问外部类的成员;其次枚举类和密封类都提供了有序的枚举值集合,区别在于密封类的定义更加严格;再次是帮助开发者摆脱搬砖命运的数据类;最后是解决未定参数类型的模板类(也叫泛型类)。
__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。