Kotlin学习(二):条件控制

178 阅读13分钟

3.1条件分支

条件分支是最简单的控制语句,主要包括两路分支以及多路分支,下面一起来看看Kotin给条件分支带来了哪些变化。

3.1.1 简单分支

最简单的分支即 if .. else..

 var is_odd = true;
        btn_if_simple.setOnClickListener {
            if (is_odd)
                btn_if_simple.text = "凉风有信的谜底是“讽”"
            else
                btn_if_simple.text = "秋月无边的谜底是“二”"
            is_odd = !is_odd
        }

上述代码可以简化为

 //缩略 类似于Java中的三目表达式 “变量名=条件语句?取值A:取值B”
        btn_if_simple2.setOnClickListener {
            btn_if_simple2.text = if (is_odd) "凉风有信的谜底是“讽”" else "秋月无边的谜底是“二”"
            is_odd = !is_odd
        }

3.1.2 多路分支

相反的是,Kotlin对 多路分支的功能做了大幅扩充,当然由于原来的switch/case机制存在局限,故而Kotlin推出新的关键字,即用when/else来处理多路分支的条件判断。 例如:

var count = 0;
      btn_when_simple.setOnClickListener {
          when (count) {
              0 -> btn_when_simple.text = "凉风有信的谜底是“讽”"
              1 -> btn_when_simple.text = "秋月无边的谜底是“二”"
              else -> btn_when_simple.text = "此刻我想吟诗一首"
          }
          count = (count + 1) % 3
      }

从以上代码可以看出when/else与switch/case有以下几点区别:

  • 关键字switch被when取代
  • 判断语句 "case:常量值 " 被 "常量值->"取代
  • 每个分支后面的break语句被取消,Kotlin默认一个分支处理完直接跳出多路语句
  • 关键字default被else取代 跟优化后的if/else一样,Kotlin中 的when/else也允许有返回值,所以上面的多路分支代码可优化为如下代码:
  //优化
        btn_when_simple2.setOnClickListener {
            btn_when_simple2.text = when (count) {
                0 -> "凉风有信的谜底是“讽”"
                1 -> "秋月无边的谜底是“二”"
                else -> "此刻我想吟诗一首"
            }
            count = (count + 1) % 3
        }

以往Java在使用switch/case时有个限制,就是case后面只能跟常量,不能跟变量,否则编译不通过。现在Kotin去掉了这个限制,进行分支处理时允许引入变量判断,当然引入具体的运算表达式也是可以的。引入变量判断的演示代码如下:

 //Kotlin允许进行分支的时候引入变量,也可以引入具体的运算表达式

        var odd: Int = 0
        var even: Int = 1
        btn_when_simple3.setOnClickListener {
            btn_when_simple2.text = when (count) {
                odd -> "凉风有信的谜底是“讽”"
                even -> "秋月无边的谜底是“二”"
                else -> "此刻我想吟诗一首"
            }
            count = (count + 1) % 3
        }

引入变量判断只是Kotlin牛刀小试,真正的功能扩充还在后面。原来的switch/case机制中,每个case仅仅对应一一个常量值,如果5个常量值都要进入某个分支,就只能并列写5个case语句,然后才跟上具体的分支处理语句。现在when/else 机制中便无须如此麻烦了,这5个常量值并排在一- 起用逗号隔开即可,如果几个常量值刚好是连续数字,可以使用“in开始值.结束值”指定区间范围;举-反三,若要求不在某个区间范围,则使用语句“!in开始值.结束值”。扩展功能后的多路分支代码举例如下:

         //kotlin允许多个常量值并排书写,
        // 如果几个常量值刚好是连续数字,可以使用“in 开始值..结束值”指定区间范围
        // 如果要求不在某个区间范围,则使用语句“!in 开始值..结束值”

        btn_when_simple4.setOnClickListener {
            btn_when_simple4.text = when (count) {
                1, 3, 5, 7, 9 -> "凉风有信的谜底是“讽”"
                in 13..19 -> "秋月无边的谜底是“二”"
                !in 6..10 -> "你猜我猜不猜"
                else -> "此刻我想吟诗一首"
            }
            count = (count + 1) % 20

        }

3.1.3 类型判断

在Kotlin中,关键字instanceof被is所取代,下 面是类型判断的Kotlin代码格式:

  var str = ""
       if (str is String) {
//            ..
       }

同时,多路分支的when/else语句也支持类型判断,只不过在分支判断时采取"is变量类型->”这种形式。下面是演示类型判断的Kotlin代码,在变量countType为Long、Double、Float三种类型时做多路判断处理:

  //when/else 引入多路判断

        var countType: Number
        btn_when_simple5.setOnClickListener {
            count = (count + 1) % 3
            countType = when (count) {
                0 -> count.toLong()
                1 -> count.toDouble()
                else -> count.toFloat()
            }
            btn_when_simple5.text = when (countType) {
                is Long -> "此恨绵绵无绝期"
                is Double -> "树上鸟儿成双对"
                else -> "门泊东吴万里船"
            }
        }

3.2循环处理

3.1节介绍了简单分支与多路分支的实现,控制语句除了这两种条件分支之外,还有对循环处理的控制,本节接下来继续阐述Kotlin如何对循环语句进行操作,看看Kotlin引入了哪些新思维。

3.2.1 遍历循环

  • Java遍历集合可以通过“for(Object item:list)"形式进行循环操作
  • Kotlin 区别在于将“:”替换成“in",eg:for(item in list)
    val poemArray: Array<String> = arrayOf("朝辞白帝彩云间", "千里江陵一日还", "两岸猿声啼不住", "轻舟已过万重山")

        tv_cycle.setOnClickListener {
            var poem: String = ""
            for (item in poemArray) {
                poem = "$poem$item,\n"
            }
            tv_cycle.text = poem
        }

注意到图3- 10中每行诗句都以逗号结尾,这里有个句号问题,因为每首绝句的第一一、三行末尾才是逗号,第二、四行的末尾应该是句号,所以这个循环代码得加以改进,补充对数组下标的判断,如果当前是奇数行,末尾就加逗号;如果当前是偶数行,末尾就加句号。倘若使用Java编码,要是涉及下标的循环,基本采取“for (初始的赋值语句;满足循环的条件判断;每次循环之后的增减语句)”这般形式,具体实现可参考以下的示例代码. 出人意料的是,Kotlin废除了“for (初始;条件;增减)’这个规则,若想实现上述功能,取而代之的是“for (i in数组变量.indices)'语句,其中indices 表示该数组变量的下标数组,每次循环都从下标数组依次取出当前元素的下标。根据该规则判断下标的数值,再分别在句尾添加逗号或者句号,据此改造后的Kotlin代码如下所示:

//改进--偶数句号,奇数逗号
        tv_cycle.setOnClickListener {
            var poem: String = ""
            //indices表示数组变量对应的下标数组
            for (i in poemArray.indices) {
                if (i % 2 == 0)
                    poem = "$poem${poemArray[i]},\n}"
                else poem = "$poem${poemArray[i]}。\n}"
            }
            tv_cycle2.text = poem
        }

3.2.2 条件循环

Kotlin的“for(i in 数组变量.indices)”语句就无法处理一下情况:

  • 如何设定条件判断的的起始值和终止值?
  • 每次循环之后的递增值不是1的时候怎么办?
  • 循环方向不是递增而是递减,又如何是好?
  • 与条件判断有关的变量不止一个,怎么办?
  • 循环过程中的变量,在循环结束后还能不能使用? 针对以上情况,其实Kotlin也给出了几个解决办法,代价是多了几个诸如until、step、downTo这样的关键字,具体的用法例子参见下列代码:
 //左闭合右开区间,合法值包括11,但不包括66
       for (i in 11 until 66) {
       }

       //每次默认递增1,这里改为每次递增4
       for (i in 23..29 step 4) {
       }

       //for循环默认递增,这里使用downTo表示递减
       for (i in 50 downTo 70) {
       }
while循环
   tv_cycle3.setOnClickListener {
            var poem = ""
            var i = 0;
            while (i < poemArray.size) {
                if (i % 2 == 0)
                    poem = "$poem${poemArray[i]},\n"
                else
                    poem = "$poem${poemArray[i]}。\n"
                i++
            }
            poem = "${poem}该诗歌一共有${i}句。"
            tv_cycle3.text = poem

        }
do-while循环
 tv_cycle4.setOnClickListener {
            var poem = ""
            var i = 0
            do {
                if (i % 2 == 0)
                    poem = "$poem ${poemArray[i]},\n"
                else poem = "$poem ${poemArray[i]}。\n"
                i++
            } while (i < poemArray.size)

            poem = "${poem}该诗歌一共有${i}句。"
            tv_cycle4.text = poem
        }

3.2.3 跳出多重循环

  • 结束当前循环 ,继续下一个循环 ---continue
  • 结束整体循环-----break
 val poem2Array: Array<String?> = arrayOf(
            "朝辞白帝彩云间", null, "千里江陵一日还", "",
            "两岸猿声啼不住", "  ", "轻舟已过万重山", "送孟浩然之广陵"
        )

        tv_cycle5.setOnClickListener {
            var poem = ""
            var pos = -1
            var count = 0

            while (pos <= poem2Array.size) {
                pos++
                //若发现该行是空串或者空格串,则忽略改行
                if (poem2Array[pos].isNullOrBlank()) continue
                if (count % 2 == 0)
                    poem = "$poem ${poemArray[pos]},\n"
                else
                    poem = "$poem ${poemArray[pos]}。\n"
                count++
                if (count == 4)
                    break

            }
            tv_cycle5.text = poem

        }

看来合法性判断用到的continue和break, Kotlin并没有做什么改进呀?这是真的吗?以往使用Java操作多层循环的时候,有时在内层循环发现某种状况,就得跳出包括外层循环在内的整个循环。例如遍历诗歌数组,一旦在某个诗句中找到“一”字,便迅速告知外界“我中奖啦”之类的欢呼。可是这里有两层循环,如果使用Java编码,只能先跳出内层循环,然后外层循环通过判断标志位再决定是否跳出,而不能从内层循环直接跳出外层循环。

现在Kotlin大笔一挥,不用这么麻烦,咱想跳到哪里就跳到哪里,只消给外层循环加个@标记,接着遇到情况便直接跳出到这个标记,犹如孙悟空蹦上筋斗云,想去哪儿就去哪儿,多方便。这个创意真好,省事省力省心,赶紧看看下面的Kotlin代码是怎么实现的:

//Kotlin跳出内层循环在跳出外层循环--->只需要外层循环加个@标记

        tv_cycle6.setOnClickListener {
            var i = 0;
            var is_found = false
            //给外层循环添加个名字叫outside的标记
            outside@ while (i < poemArray.size) {
                var j = 0
                var item = poemArray[i]
                while (j < item.length) {
                    if (item[j] == '一') {
                        is_found = true
                        break@outside
                    }
                    j++
                }

                //按照以前的思路就是添加标记,内层循环返回true,在结束外层循环

//                if (is_found)
//                    break

                i++

            }
            tv_cycle6.text = if (is_found) "我找到‘一’字了" else "没有找到滴滴滴"
        }

3.3 空安全

3.3.1 字符串的有效性判断

开发中最让人头痛的当数空指针异常,该异常频繁发生却又隐藏很深。一旦调用某个空对象的方法,就会产生空指针异常。可是Java编码的时候编译器不会报错,开发者通常也意识不到问题,只有App运行之时发生闪退,查看崩溃日志才会恍然大悟,“原来这里得加上变量非空的判断”。 问题的症结在于,Java编译器不会检查空值,只能由开发者在代码中手工增加“if(!=null)”的分支判断。但是业务代码里面的方法调用浩若繁星,倘若在每个方法调用之前都加上非空判断,势必大量代码都充满了“if ( != null)”。这样做的后果不仅降低了代码的可读性,而且给开发者带来不少的额外工作量。

此外,空指针只是狭义上的空值,广义上的空值除了空指针外,还包括其他开发者认可的情况。比如说String类型,字符串的长度为0时也可算是空值;如果字符串的内容全部由空格组成,某种意义上也是空值。那么对于字符串的非空判断,用Java书写见下面的示例代码:

if(str!=null && str.length()>0 && str.trim().length>0){...} Kotlin 校验字符串空值的几个方法:

  • isNullOrEmpty :为空指针或者字符串的长度为0时返回true,非空串与可空串均可调用
  • isNullOrBlank :为空指针、字符串长度为0、或者全为空格的时候返回True,非空串与可空串均可调用
  • isEmpty :字符串长度为0时返回true,只有非空串可调用
  • isBlank : 字符串长度为0或者全为空格时返回true,只有非空串可调用
  • isNotEmpty :字符串长度大于0时返回true,只有非空串可调用
  • isNotBlank : 字符串长度大于0且不是全空格串时返回true,只有非空串可调用

3.3.2 声明可空变量

字符串空值校验方法有区分非空串与可空串,这是缘于Kotlin引入了空安全的概念,每个类型的变量都分作不可为空和可以为空两种。前面的文章中,正常声明的变量默认都是非空(不可为null),比如下面声明字符串变量的代码:

       //不可空变量
        var strNotNull: String = ""
        //可空变量 --不能访问isEmpty、isBlank、isNotEmpty、isNotBlank方法
        var strCanNull: String?

**非空变量要么在声明时就赋值,要么在方法调用前赋值;否则未经初始化就调用该变量的方法,Kotlin会 像语法错误那样标红提示:“Variable *** must be initialized"。**至于可以为空的变量,可于声明之时在类型后面加个问号,如同“3.2.3跳出多重循环'声明可空字符串数组的代码“val poem2Array:Array<String?>= ***”。若只声明一个可空字符串变量,则具体的代码例子如下所示: var strCanNull:String?

现在定义了两个字符串,其中strNotNull为非空 串,strCanNull为 可空串。按照前面几个字符串空值校验方法的规则,strNotNull允许调用 全部6个方法,但strCanNull只允许调用isNullOrEmpty和isNullOrBlank两个方法。因为变量strCanNull可能为空,若去访问一个空字符串的length属性,毫无疑问会扔出空指针异常,所以Kotlin对可空串增加编译检查,一旦发现某个可空的字符串变量调用了非空方法,比如isEmpty、isBlank、 isNotEmpty、isNotBlank等,则Android Studio立刻标红提示此处存在语法错误:“Only*** calls are allowed on a nullable receiver of type String”。

可是上述的几个is***方法局限于判断字符串是否为空串, **如果要求获得字符串的长度,或者调用其他的字符串方法,此时仍然要判断空指针。**以获取字符串长度为例,下面声明三个字符串变量,其中strA为非空串,strB和strC都是 可空串,不过strB 为空而strC实际有值,字符串变量的声明代码如下:

        var strA: String = "非空"
        var strB: String? = null
        var strC: String? = "可空串"

对于strA,因为它是非空串,所以可直接获取length长度属性。对于strB和strC必须进行非空判断,否则编译器会提示该行代码存在错误。这三个字符串的长度获取代码如下所示:

        var strA: String = "非空"
        var strB: String? = null
        var strC: String? = "可空串"

        var length: Int = 0
        tv_panduan.setOnClickListener {
            length = strA.length
            tv_panduan.text = "字符串A的长度为$length"
        }
        tv_panduan2.setOnClickListener {
                   length = if (strB != null) strB.length else -1
                  tv_panduan.text = "字符串B的长度为$length"
        }
        tv_panduan3.setOnClickListener {
            length = if (strC != null) strC.length else -1
            tv_panduan.text = "字符串C的长度为$length"
        }

3.3.3 检验空值的运算符

虽然使用条件分支可以完成非空判断的功能,可是Kotlin仍旧嫌它太哕唆。既然访问空串的length属性会扔出空指针异常,那就加个标记,告诉编译器遇到空指针别扔异常,直接返回空指针就好了,至少避免了处理异常的麻烦。对应的Kotlin标记代码如下:

 // 改进:?.-->相当于leng_null=if(strB!=null)strB.length else null
        var length_null: Int?
        tv_panduan4.setOnClickListener {
            //?.表示变量为空时直接返回null,所以返回值的变量必须声明为可空类型
            length_null = strB?.length
            tv_panduan4.text = "使用?.得到字符串B的长度为$length_null"
        }

从以上代码可以看到,这个多出来的标记是个问号,语句“strB?.length"其实等价于“length_ null = if (strB!=null) strB.length else null".但是,该语句意味着返回值仍然可能为空,如果不想在界面上展示“null",还得另外判断length null是否为空;也就是说,这个做法并未实现与原代码完全一致的功能。

没有完成任务,Kotlin当 然不会罢休,所以它又引入了一个新的运算符“?:”,学名叫作“Elvis操作符”,叫起来有点拗口,读者可以把它当作是Java的三元运算符“变量名=条件语句?取值A:取值B' '的缩写。引入运算符“?:”的实现代码如下所示:

 //改进:?:-->相当于Java的三目表达式“变量名=条件语句?取值A:取值B”

        tv_panduan5.setOnClickListener {
            //?:跟在变量后面,表示变量为空时就返回右边的值,即(x!=null)?x.**:y
            length = strB?.length ?: -1
            tv_panduan5.text = "使用?:得到字符串B的长度为$length"
        }

这样总该完事了吧?然而执拗的Kotin工程师觉得还是哕嗦,因为经常上-行代码就对字符串strB赋值了,所以此时可以百分之百保证strB非空,那又何必浪费口舌呢?于是Kotlin引入了另一种运算符"!!”,表示甭管那么多,前方没有地雷,弟兄们赶紧上。把双感叹号加在变量名称后面表示强行把该变量从可空类型转为非空类型,从而避免变量是否非空的校验。下面是运算符“!!”的使用代码例子:

   //改进:!!-->双感叹号加在变量名称后面表示强行把该变量从可空类型转为非空类型,此时编译器不做非空校验,但是任然可能为空,需要做异常处理


      tv_panduan6.setOnClickListener {
            strB = "排雷完毕"

            try {
                length = strB!!.length
                tv_panduan6.text = "使用!!得到字符串B的长度为$length"
            } catch (e: Exception) {
                tv_panduan6.text = "发现空指针异常了"
            }

        }

总结: (1)声明变量实例是,在类型名称后面加问号,表示该变量可以为空 (2)调用变量方法时,在变量名称后面加问号,表示一旦变量为空就返回null (3)新引入运算符"?:",表示一旦变量为空,就返回该运算符右边的表达式 (4)新引入运算符"!!",表示通知编译器不做非空校验。如果运行时发现变量为空,就扔出异常

3.4 等式判断

3.4.1 结构相等

字符串的等值型判断要求Java的判断方式Kotlin的判断方式
判断两个字符串是否相等strA.equals(strB)strA==strB
判断两个字符串是否不等!strA.equals(strB)!strA==strB
  • 凡是Java中实现了equals函数的类,其变量均可在Kotlin中通过运算符“==”和“!=”进行等式判断
  • 这种不比较存储地址,而是比较变量结构内部值的行为,kotlin称之为结构相等,即模样相等

3.4.2 引用相等

在Kotlin中,结构相等的运算符是双等号“==”,那么引用相等的运算符便是三个等号“===”,多出来的一个等号表示连地址都要相等;结构不等的运算符是“!=”,相对应地,引用不等的运算符是“!==”。不过在大多数场合,结构相等和引用相等的判断结果是一致的。下面列出几种常见的等式判断情景。

  • 对于基本数据类型,包括整型、浮点型、布尔型、字符串、结构相等和引用相等没有区别
  • 同一个类声明的不同变量,只要有一个属性不相等,则其既是结构不等,也是引用不等
  • 同一个类声明的不同变量,若equals方法校验的每个属性都相等(譬如clone),则其结构相等,但引用不等
        val date1: Date = Date()
        val date2: Any = date1.clone()//从date1原样克隆一份到date2
        tv_equals.setOnClickListener {
            when (count++ % 4) {
                0 -> {
                    tv_check_title.text = "比较date1和date2是否结构相等"
                    //结构相等比较的的是二者的值
                    val result = date1 == date2
                    tv_equals.text = "==的比较结果是$result"
                }
                1 -> {
                    tv_check_title.text = "比较date1和date2是否结构不相等"
                    //结构不相等比较的的是二者的值
                    val result = date1 != date2
                    tv_equals.text = "==的比较结果是$result"
                }
                2 -> {
                    tv_check_title.text = "比较date1和date2是否引用相等"
                    //引用相等比较的的是二者是不是同一个东西,即使克隆也不是同一个东西
                    val result = date1 === date2
                    tv_equals.text = "===的比较结果是$result"
                }
                else -> {
                    tv_check_title.text = "比较date1和date2是否引用不相等"
                    //引用相等比较的的是二者是不是同一个东西,即使克隆也不是同一个东西
                    val result = date1 !== date2
                    tv_equals.text = "!==的比较结果是$result"
                }
            }
        }

3.4.3 is和in

运算符is 和!is

  • 校验变量是否为某种类型,使用的是关键字is。-->"变量名称 is 类型名称“
  • 如果校验变量是否不为某种类型,使用的关键字是!is,-->“变量名称 !is 类型名称”
  val oneLong: Long = 1L
        var isEqual: Boolean = true
        tv_is.setOnClickListener {
            if (isEqual) {
                val result = oneLong is Long
                tv_is.text = "比较oneLong是否是长整型\nis的比价结果是$result"
            } else {
                val result = oneLong !is Long
                tv_is.text = "比较oneLong是否非长整型\nis的比价结果是$result"
            }
            isEqual = !isEqual
        }

运算符in 和!in

  • 检验数组中是否存在等值元素,使用关键字 in.--->“变量名 in 数组名”
  • 检验数组中是否不存在等值元素,使用关键字 !in.--->“变量名 !in 数组名”
    val oneArray: IntArray = intArrayOf(1, 2, 3, 4, 5)
        val four: Int = 4
        val nine: Int = 9

        tv_in.setOnClickListener {
            when (count++ % 4) {
                0 -> {
                    val result = four in oneArray
                    tv_in.text = "比较$four 是否在数组oneArray中\nin的比较结果是$result"
                }
                1 -> {
                    val result = four !in oneArray
                    tv_in.text = "比较$four 是否不在数组oneArray中\n!in的比较结果是$result"
                }
                2 -> {
                    val result = nine in oneArray
                    tv_in.text = "比较$nine 是否在数组oneArray中\nin的比较结果是$result"
                }
                else -> {
                    val result = nine !in oneArray
                    tv_in.text = "比较$nine 是否不在数组oneArray中\n!in的比较结果是$result"
                }
            }
        }