• 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}
    " +
                    "第一个植物的描述是${lotus.toString()}
    " +
                    "第二个植物的描述是${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的六种特殊函数,首先嵌套类和内部类都定义在某个外部类的内部,区别在于能否访问外部类的成员;其次枚举类和密封类都提供了有序的枚举值集合,区别在于密封类的定义更加严格;再次是帮助开发者摆脱搬砖命运的数据类;最后是解决未定参数类型的模板类(也叫泛型类)。

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

  • 相关阅读:
    CCF CSP 题解
    CCF CSP 2019032 二十四点
    CCF CSP 2018121 小明上学
    CCF CSP 2019092 小明种苹果(续)
    CCF CSP 2019091 小明种苹果
    CCF CSP 2019121 报数
    CCF CSP 2019031 小中大
    CCF CSP 2020061 线性分类器
    CCF CSP 2020062 稀疏向量
    利用国家气象局的webservice查询天气预报(转载)
  • 原文地址:https://www.cnblogs.com/aqi00/p/7410118.html
Copyright © 2020-2023  润新知