天天看点

Kotlin学习---Kotlin基础

Kotlin学习这个系列文章是我在阅读了 《Kotlin In Action》 书籍之后,按照书籍中的目录结构写的。有几点需要说明一下:

  1. 写这个系列的文章,首先是想巩固一下自己学习的东西,同时查漏补缺。其次也希望让更多的人能够学习和了解到Kotlin这门语言。
  2. 大家看完之后有任何的疑惑,意见和建议都可以提出来,我们可以一起探讨。
  3. 书籍中分两大部分,一是Kotlin简介(当然不是字面上的简单介绍一下Kotlin,学习完这一部分可以说已经入门Kotlin了。预计 不会少于5篇文章 ),二是拥抱Kotlin(这一部分主要讲了很多Kotlin中的高级用法)。这个系列的文章主要是第一部分的描述,如果大家看了之后想了解第二部分的内容可以买 《Kotlin In Action》 书籍自己来看。
  4. 本篇文章阅读之前需要对Java有一定的了解,如果对Java还不熟悉的还是先去把Java学习一遍在来看。
  5. 最后我想说的是 《Kotlin In Action》 是一本很好的学习Kotlin的书籍,墙裂推荐大家去买一本看看。

    让我们开始Kotlin的学习之旅吧。Let’s Go!

文章目录

    • 1.1基本要素:函数和变量
      • 1.1.1 函数
      • 1.1.2 变量
      • 1.1.3 更简单的字符串格式化:字符串模板
    • 1.2 类和属性
      • 1.2.1 属性
      • 1.2.2 自定义访问器
      • 1.2.3 Kotlin源码布局:目录和包
    • 1.3 表示和处理选择:枚举和 “when”
      • 1.3.1 声明枚举类
      • 1.3.2 使用 “when” 处理枚举类
      • 1.3.3 在 “when” 结构中使用任意对象
      • 1.3.4 使用不带参数的 “when”
      • 1.3.5 智能转换:合并类型检查和转换
      • 1.3.6 重构:用 “when” 代替 “if”
      • 1.3.7 代码块作为 “if” 和 “when” 的分支
    • 1.4 迭代事物:“while” 循环和 “for” 循环
      • 1.4.1 while 循环
      • 1.4.2 迭代数字:区间和数列
      • 1.4.3 迭代map
      • 1.4.4 使用 “in” 检查集合和区间的成员
    • 1.5 Kotlin中的异常
      • 1.5.1 “try” “catch” 和 “finally”
      • 1.5.2 “try” 作为表达式
    • 总结

先大致说一下这篇文章要写的内容:

  • 在Kotlin中如何声明函数、变量、类、枚举以及属性
  • Kotlin中的控制结构
  • 智能转换(Kotlin是跟Java一样的静态语言,但是Kotlin又做到了像JS这样的动态语言一样在声明变量时不需要知道变量类型,这就要了解智能转换的概念了)
  • Kotlin中对异常的处理

1.1基本要素:函数和变量

首先我们先看一段代码

//Kotlin
fun main(array: Array<String>) {
    println("Hello,world!")
}

//Java
void main(String[] array) {
    System.out.println("Hello,world!");
}
           

俗话说的好学习一门语言都从输出 “Hello,world!” 开始。从这一段代码中我们就可以看到Kotlin的很多特性和语法了

  • Kotlin中声明一个函数需要用到 fun 这个关键字,Java中不需要特定的什么关键词来声明函数。
  • Kotlin中方法参数的类型是写在参数后面的,Java中则相反。
  • Kotlin中的 数组是用Array这个泛型类表示的 ,而在Java中数组是一个特殊的类型(Java中的类型分为 基本类型 ,引用类型 和 数组类型 )
  • Kotlin中使用println代替了Java中的System.out.println。Kotlin标准库给Java标准库提供了大量语法更简洁的包装。
  • Kotlin中 不需要 在每行代码结尾 添加分号表示结束 。(有一个特例,后面会讲到)
  • 最后还有一点隐藏的特性,Kotlin中的 函数不需要定义在类中 ,可以定义在类外部甚至随便一个Kotlin文件中。这跟Java有很大的不同。

1.1.1 函数

下面我们用一张标注图来表示Kotlin函数的基本结构:

Kotlin学习---Kotlin基础

很直观的可以看出跟Java的函数结构几乎是一样的。都包括这几部分:

  1. 函数名称
  2. 函数参数
  3. 返回类型
  4. 函数体

老司机们一看这个结构就能够和java关联起来了,so easy嘛。wait!

return if (a > b) a else b
           

是什么鬼啊?!怎么跟Java中的三元操作符 ?: 一样?这就要说到Kotlin和Java中 语法 和 表达式 的区别了。

语法和表达式

语法和表达式的区别在于,表达式是有值的,并且可以嵌套在另一个表达式中;而语法是对代码块的封装,本身是没有值的。对于Kotlin来说除了循环(for、do和do/while)以外的控制结构都是表达式(例如上面代码中的if),而Java中所有的控制结构都是语法。(需要特别注意的是,在Java中赋值操作是表达式,而Kotlin中则是语法。这样做是为了避免和比较操作符的混淆)后面会经常看到控制结构和其他表达式结合在一起的情况。

对于上面的函数我们可以对它简化成下面的样子:

fun max(a: Int, b: Int): Int = if (a > b) a else b
           

因为这个函数体只有一个表达式,所以可以直接用表达式来当函数体同时可以省略掉花括号和return。这样子看起来会非常的简洁。一般的如果函数体在花括号里面,就说这个函数有 代码块体。如果它直接返回了一个表达式,就说它有 表达式体。

在进一步简化,可以写成下面的代码:

fun max3(a: Int, b: Int) = if (a > b) a else b
           

这次直接把返回类型给去掉了。这里就设计到Kotlin的智能转换了,后面会讲到。同时注意只有 表示式体函数 才能把返回类型省略, 代码块体函数 必须显式写出具体的返回类型。

1.1.2 变量

说完函数,咱们再来说说Kotlin中的变量。照例先来看代码

val question = "The Ultimate Question of Life, the Universe, and Everything"

val answer = 42

// val answer: Int = 42
    
var yearsToCompute = 7.5e6  //7.5*10^6 = 7500000.0
           

通过比较 question 和 yearsToCompute 这两个变量,我们发现存在两个修饰符,var,val,这在Kotlin中代表可变变量和不可变变量。

  • val(来自 value)— 不可变引用 。使用 val 声明的变量不能在 初始化之后 再次赋值。它对应的就是Java中的 final 变量
  • var(来自variable)— 可变引用 。这种变量的值是可以改变的。这种声明对应的就是Java中的普通(非 final )的变量。

Kotlin中大部分时候 推荐使用val来声明变量,仅在必要的时候换成var 。通常情况下,使用 不可变引用,不可变集合及无副作用的函数 会让你的代码更加的接近函数式编程风格。

上面说到 val 声明的变量不能在初始化之后赋值,但是如果编译器能够保证 在变量声明周期内只有一条初始化语句 ,那就可以通过不同的条件来初始化它比如:

val message: String
if (canPerformOperation()) {
    message = "Success"
} else {
    message = "Failed"
}
           

注意,尽管val是不可变的,但是它指向的对象可能是可变的。例如:

val languages = arrayListOf("Java")
languages.add("Kotlin")
           

这段代码的意思就是声明了一个不可变变量,并指向了一个ArrayList集合的内存地址。其中变量指向的ArrayList这个集合容器的地址是不能修改的,但是里面具体的元素对应的内存地址是可以修改的。

接下来我们比较一下 question 和 answer 这两个变量,他们的初始化值分别是String类型的和Int类型的,但是他们都没有声明对应的类型。这个和表达式体函数一样,编译器会分析初始化器表达式的值,并把表达式的类型作为变量的类型。如果显式的表达的话就是第三个变量的声明方式。这让Kotlin的变量变的像JS这种动态语言一样,可以 省略变量类型/函数返回值类型 ,显的格外的简洁。

需要注意的是

var answer = 42
answer = "no answer" //wrong
           

在初始化之后,变量的类型是不能够被改变的。

1.1.3 更简单的字符串格式化:字符串模板

在了解了Kotlin的函数和变量之后,我们在来学习一下Kotlin的新特性,字符串模板。

在平时开发的过程中肯定会经常遇到在一段字符串中几个值需要动态的去设置的。这个时候我们会通过这几种方式来处理:

void printValue(String value) {
    System.out.print("value:" + value);
    //或者
    System.out.print(String.format("valur:$s" + value));
}
           

可以说是信手捏来,但是这两种方式都会给人一种割裂感,那Kotlin中是如果处理的呢。直接看代码:

fun printValue(value: String) {
    println("value:${value}")
}
           

是不是感觉紧凑多了,直接在双引号里面就完成了字符串的动态添加。这个在Kotlin中就叫做 字符串模板 。

使用也非常的方便只需要在字符串中通过 在变量(或者是表达式)前添加 $ 符号,就可以引用了。需要注意的是如果想要输出 $ 符号,需要通过反斜杠 \ 来转义。

1.2 类和属性

这一小节中,让我们大致了解一下Kotlin中的类(在后面文章中会有详细的介绍),同时引出Kotlin中 属性 这个概念,话不多说直接上代码。

public class Person {
    private final String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
           

这是一个Java中很常见的Bean类,里面只有一个私有的final类型的变量name,一个构造函数和一个name对应的get方法。这其中很多都是样板代码,比如说对name字段的赋值,对应的get方法等等,那么在Kotlin中要代表这样一个类需要怎么表达呢?

对,就是这么简单,一行代码就可以表达Java中需要九行代码才能表达的意思了。通常我们把这种只有数据没有其他代码的类叫做 值对象。注意在Kotlin中类默认是public的,所以默认可以省略。

Kotlin中在类后面括号的部分叫做 主构造器,这部分概念后面文章会详细说。

1.2.1 属性

在上面Person类中,像name这种字段和其对应的get方法(还有set方法,一起叫做访问器方法)这种组合通常被叫做属性。在Kotlin中 属性是头等的语言特性 ,完全代替了字段和访问器方法。声明属性也是用 val 和 var 关键字,分别表示 不可变属性 和 可变属性 。

在前面我们说到变量,在用法上跟属性一毛一样,可能有人会问了这两个有什么区别,在我理解看来其实这两个概念只是对应的范畴不同,你可以说声明了一个变量,也可以说声明了一个属性。变量对应的是常量,属性则是对字段和访问器方法这种组合的叫法。需要说明的是在Kotlin中也存在 字段 ,叫做 Backing Field(在后面章节会详细的说),但是它 只存在于访问器 中,其他任何地方都没有 字段 这个概念。

首先我们来看一下声明一个完整属性的语法:

var <PropertyName>[: <PropertyType>] [= <Property_initializer>]
[<getter>]
[<setter>]
           
  • PrpertyName:属性名称
  • PropertyType:属性类型
  • Property_initializer:属性初始化器
  • getter,setter:get和set方法

其中Property_initializer,getter,setter都是可选的,如果初始化器可以推断属性的类型,那么PropertyType也是可以省略的。

接下来我们看一下Kotlin和Java中分别该如何使用 属性 。我们先声明一个Person类:

class Person(
    val name: String,  //只读属性:编译器会对应的生成一个字段和一个对应的getter方法
    var isMarried: Boolean //可写属性:编译器会生成一个字段和对应的getter,setter方法
)
           

如果把这段代码编译成字节码,和Java中同样的Bean类编译成字节码,这两者几乎没有什么差别。话不多说,让我们来看一下Java中和Kotlin中如何调用这个Person类的。

//Java
Person person = new Person("Bob", true);
System.out.print(person.getName());
System.out.print(person.isMarried());
    
//Kotlin
val person = Person("Bob", true)
print(person.name)
print(person.isMarried)
           

从这段代码中可以得出下面一些结论:

  • Java中获取name属性的值通过对应的 get方法 ,如果name是可写的属性的话还有 set方法 。
  • 如果Kotlin中的属性是 is 开头的,那么对应的获取方法不会在属性前面添加 get前缀 ,设置方法则是把is替换成set,而不是直接在属性前面添加set前缀。
  • 如果需要修改字段的值,在Java中调用对应的set方法,Kotlin直接通过 isMarried = true 来修改(实际是调用对应的set访问器方法)
  • 同样的,在Java中定义的类,也是通过一样的方法调用。

1.2.2 自定义访问器

如果我需要统计一个变量的访问次数或者修改次数,或者对设置的值做一个校验,在Kotlin中该如何实现呢?这个时候就需要自定义属性的访问器了。上代码:

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() {
           return height == width
        }
}
           

我们定义了一个矩形类,其中有一个属性用来表示是否是正方形。

可以看到我们重写了get方法,在其中通过判断宽高是否相等来得出是否是正方形。

同样的我们可以直接用表达式体函数来简化:

class Rectangle(val height: Int, val width: Int) {
        val isSquare: Boolean
             get() = height == width
        }
      
      
      //对比默认的getter和setter实现
var language: String = "Kotlin"
        get() {
            return field  //前面说到的 Backing Field
        }
        set(value) {
            field = value
        }
           
一个没有参数的函数和声明带自定义getter的属性哪个比较好?其实两者之间在性能上没有什么差别,唯一的差异在于可读性上。一般如果是当前类的特征,应该把它声明成属性。

1.2.3 Kotlin源码布局:目录和包

大致了解了Kotlin中的类,函数和属性,咱们在来说说更高一层的,Kotlin中的目录和包结构。

直接上结论,Kotlin完全可以拿Java的目录结构来使用,但是也有一些不一样的点:

  • Java和Kotlin都是通过import来引入其他包的类的。相比Java,Kotlin可以通过import引入包中具体类的函数,例如:
package geometry.example

    
import geometry.shape.createRandomRectangle  //直接导入函数
//也可以import geometry.shape.* 也可以这样引入包中所有的类,属性和函数

fun main(args: Array<String>) {
      print(createRandomRectangle().isSquare) //使用就跟调用当前类函数一样
}
           
  • Kotlin中可以把多个类放在一个文件中,文件名字也可以随意选择,也没有对磁盘上源文件的布局强加任何限制,例如:

    Kotlin中

    Kotlin学习---Kotlin基础
    Java中
    Kotlin学习---Kotlin基础
    在shape文件中包含了Rectangle和RectangleUtil类中的内容。关于如何调用文件中的类方法和属性,将在后面章节中介绍。

Kotlin在目录结构上非常的灵活,但是大多数情况下参照Java的目录布局来创建文件依然是不错的实践。尤其是Java代码和Kotlin代码混合的项目,可以避免很多错误和误解的情况。

1.3 表示和处理选择:枚举和 “when”

这一章让我们了解一下Kotlin中的枚举,和Kotlin中的 “switch case” 语法

1.3.1 声明枚举类

首先让我们创建一个包含颜色的枚举类:

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
           
  • 可以看出要声明一个Kolint的枚举类,需要使用 enum class 两个关键词,而Java中只需要一个 enum 关键词就够了,这也是极少数在Kotlin中声明比Java使用更多关键词的例子。
  • enum 关键词在Kotlin中是 软关键词 ,它只有在跟 class 关键词一起出现的时候才会表示枚举,其他时候就是一个普通的名称,没有特殊的意义。这跟Java是不同的,在Java中 enum 就代表着枚举类,不能当作一个普通的名称来使用。

Kotlin中的枚举跟Java一样,不单单是一个值的列表,它也可以声明函数和属性,如下面的代码:

//声明枚举常量
enum class Color(val r: Int, val g: Int, val b: Int) {
    RED(255, 0, 0),
    ORANGE(255, 165, 0),
    YELLOW(255, 255, 0),
    BLUE(0, 0, 255),
    INDIGO(75, 0, 130),
    VIOLET(238, 130, 238),
    GREEN(0, 255, 0); //在枚举类中声明了方法后,在末尾必须有一个分号,来区分方法和实例。这也是Kotlin中唯一需要在末尾添加分号的地方
    
    fun rgb() = (r * 256 + g) + b //给枚举类定义了一个方法
}
           

Kotlin中枚举的使用跟Java基本上是一致的,你在Java中能够声明函数,变量等等,你在Kotlin中同样都可以做到。

1.3.2 使用 “when” 处理枚举类

在Kotlin中要达到Java中 switch case 语句的效果就要使用到 when 这个控制结构了。通过跟枚举类的结合,我们来看看该怎么使用 when。

fun getMnemonic(color: Color) = //直接返回一个 when 表达式
        when (color) {      //如果颜色和枚举类相等就返回对应的字符串
            Color.BLUE -> "Blue1"
            Color.RED -> "Red2"
            Color.GREEN -> "Green3"
            Color.YELLOW -> "Yellow4"
        }
            
>>>  print(getMnemonic(Color.BLUE))
Blue1
           
  • 在Kotlin中 when 也是一个表达式,是有返回值的,所以上面的例子就是前面章节中提到的多行的表达式体函数。
  • 跟Java不同,不需要在每个分支末尾添加 break 。
  • 可以把多个值合并到一个分支中,只需要逗号分隔这些值。
fun getWWarmth(color: Color) =
        when (color) {
            Color.RED, Color.YELLOW -> "warm" //合并多个值到一个分支上,逗号分隔
            Color.BLUE -> "cold"
            Color.GREEN -> "neutral"
        }
>>>  print(getWWarmth(Color.RED))
warm
           

1.3.3 在 “when” 结构中使用任意对象

Kotlin中的 when 比Java中的 switch 强大的多。在Java中 switch 必须要求使用常量(枚举常量,字符串常量和数字字面值)作为分支条件,而 when 可以使用任何对象来当分支条件。下面来写一个函数来混合两种颜色,通过这个函数来看 when 是多么的强大。

fun mix(c1: Color, c2: Color) =
        when (setOf(c1, c2)) { //when 表达式的实参能够是任何对象,它被检查是否与分支条件相等
            setOf(Color.RED, Color.YELLOW) -> Color.ORANGE  //列举出能够混合的颜色对
            setOf(Color.YELLOW, Color.BLUE) -> Color.GREEN
            setOf(Color.BLUE, Color.VIOLET) -> Color.INDIGO
            else -> throw Exception("Dirty color")
        }
           
  • 颜色混合跟颜色添加的顺序是没有关系的,所以这里使用了Set集合来表示(两个set集合比较不关心元素在集合中的顺序,只要两个集合中的任一元素在另一个集合中能够找到,那么就说这两个集合是相同的。setOf()是Kotlin中方便创建set集合的一个方法)。可以看到 when 中的实参是一个set集合,分支条件也是一个set集合。
  • when 表达式会把它的实参依次和所有的分支条件做匹配,直到符合某个分支条件。可以看出来如果最后没有一个分支是匹配的,会走到 else 分支中。跟 switch 中的 default 一致。

在 when 结构中可以使用任何表达式,这会让你的代码既简洁又清晰。

1.3.4 使用不带参数的 “when”

在上面的例子中我们发现每次在做分支匹配的时候都会创建了Set对象,这个效率是很低的。我们都知道有时候为了性能,就需要在可读性上做出妥协。那让我们来看一下,怎么样提高上一小节中 mix 函数的性能吧。

fun mixOptimized(c1: Color, c2: Color) =
        when {   //没有传实参给 when
            (c1 == Color.RED && c2 == Color.YELLOW) ||
                (c1 == Color.YELLOW && c1 == Color.RED) -> Color.ORANGE
            (c1 == Color.YELLOW && c2 == Color.BLUE) ||
                    (c1 == Color.BLUE && c2 == Color.YELLOW) -> Color.GREEN
            (c1 == Color.BLUE && c2 == Color.VIOLET) ||
                    (c1 == Color.VIOLET && c2 == Color.BLUE) -> Color.INDIGO
            else -> throw Exception("Dirty color")
        }

>>>  print(mixOptimized(Color.BLUE, Color.YELLOW))
GREEN
           

如果 when 没有传实参,那么分支条件可以是任意的表达式。mixOptimized 函数跟上面的 mix 函数效果是一样的,这种写法的优点在于性能好,不会额外的创建对象,缺点就是可读性上比较差。

1.3.5 智能转换:合并类型检查和转换

前面在介绍变量的时候我们就说到过智能转换这个概念,在这一章节通过 when 结构来对这个概念有一个深入的了解。

对于(1+2)+4这样的算术你会采用怎么样的形式编码呢?我们把它存储在一个 树状结构 中,结构中每个节点要么是一次求和(Sum)要么是一个数字(Num)。Num永远都是 叶子节点 ,而Sum节点又有 两个子节点 :它们是求和运算的两个参数。直接上代码:

interface Expr
class Num(val value: Int) : Expr //简单的值对象,只有一个属性value,实现了Expr接口
class Sum(val left: Expr, val right: Expr) : Expr //Sum运算的实参可以是任何Expr:Num或者另一个Sum
           

我们用图来表示一下这个结构:

Kotlin学习---Kotlin基础

表达式Sum(Sum(Num(1),Num(2)),Num(4))的表示法

Expr接口有两种实现,所有为了计算出表达式的结果值,得尝试两种选项:

  • 如果表达式是一个数字,直接返回它的值
  • 如果是一次求和,得先计算左右两个表达式的值,再返回它们的和。

我们先按照Java的编码风格但是用Kotlin的语法来编码:

fun eval(e: Expr): Int {
        if (e is Num) {
            val n = e as Num //显式的转换成类型Num是多余的
            return n.value
        }
        if (e is Sum) {
            return eval(e.right) + eval(e.left) //变量e被智能的转换了类型
        }
        throw IllegalArgumentException("Unknown expression")
    }

>>>  println(eval(Sum(Sum(Num(1),Num(2)),Num(4))))
7
           
  • 在Kotlin中使用 is 来判断一个变量的类型。跟Java中的 instanceOf 相似。
  • 在Kotlin中用 is 检查过变量的类型之后不需要在强制转换变量的类型就可以使用检查过的类型的方法,这跟Java中不同。
  • Kotlin中的智能转换是 编译器 帮忙完成的。
  • 注意智能转换旨在变量经过is检查且之后不再发生变化的情况下有效。例如不是 val 变量,或者有 自定义的访问器 时,智能转换会失效。
  • Kotlin中使用 as 关键词来表示特定类型的显式转换。

接下来的章节让我们看一下如果用Kotlin的编程风格该如果实现这段逻辑。

1.3.6 重构:用 “when” 代替 “if”

前面我们已经了解过在Kotlin中,if 类似于Java中的 ?: 三元操作符,因为 if 是可以有返回值的。那么对于前面的 eval 函数,让我们去掉 return 语句和花括号,直接使用 if 表达式作为函数体。

fun eval(e: Expr): Int =
        if (e is Num) {
            e.value
        } else if (e is Sum) {
            eval(e.right) + eval(e.left)
        } else {
            throw IllegalArgumentException("UnKnow Exception")
        }
           
  • 如果 if 是表达式,那么花括号是可以省略的。
  • 如果 if 是代码块,代码块的最后一行会被作为结果返回。

接下来我们在用 when 来重构这段代码

fun eval(e: Expr): Int =
        when (e) {
            is Num -> //检查实参类型的 when 分支
                e.value  //这里应用了智能转换
            is Sum ->
                eval(e.right) + eval(e.left)
            else ->
                throw IllegalArgumentException("UnKnow Expression")
        }
           
  • when 表达式不仅检查值是否相等,还可以检查实参类型。
  • 可以很清楚的看到在Kotlin中如果类型已经检查过了,那么后面在使用的时候就不需要在强制转换类型了。
  • 当分支条件很多时,用 when 表达式比 if 表达式显得更加简洁清晰。

1.3.7 代码块作为 “if” 和 “when” 的分支

当分支逻辑太复杂的时候,可以用代码块作为分支体。下面就来看看要怎么来使用。

fun evalWithLogging(e: Expr): Int =
        when (e) {
            is Num -> {
                println("num:${e.value}")
                e.value  //返回代码块最后的表达式
            }
            is Sum -> {
                val left = evalWithLogging(e.left)
                val right = evalWithLogging(e.right)
                println("sum:$left+$right")
                left + right
            }
            else -> throw IllegalArgumentException("UnKnow Expression")
        }
            
            
>>>  evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))
num:1
num:2
sum:1+2
num:4
sum:3+4
7
           

分支中的代码块也遵循 最后的表达式就是结果 。

1.4 迭代事物:“while” 循环和 “for” 循环

这一章节我们看来一下Kotlin中关于循环的特性。其中 while 循环跟Java中的基本没有差别。for 循环在Kotlin中只有一种表现形式,跟Java中的 for-each 循环一致。在使用到循环的场景中,最先想到的肯定是集合,这一章节会讲到,同时也会覆盖到其他场景。

1.4.1 while 循环

Kotlin中有 while 和 do-while 循环,跟Java中的 while 和 do-while 语法没有什么区别:

while (condition){  //当 condition 为true时执行循环体
    //TODO
}
        
        
do {
    //TODO    
} while (condition) //循环体第一次会无条件的执行,此后,只有在 condition 为true时才执行
           

1.4.2 迭代数字:区间和数列

先说明一区间和数列的概念:

  • 区间:在Kotlin中没有Java里面常规的 for 循环。为了代替这种常见的循环方式,Kotlin中使用了区间的概念。区间本质上就是 两个值的间隔 一个起始值和一个结束值。使用 … 来表示区间:
  • 数列:我们把能够迭代区间中所有值的区间叫做数列。

下面用一个 Fizz-Buzz 游戏来展现一下 for 循环的用法。

Fizz-Buzz游戏是一种用来打发长途驾驶旅程的不错方式,还能帮助回忆起被遗忘的除法技巧。游戏玩家轮流递增计数,遇到能被3整除的数字就用单词fizz 代替,遇到能被5整除的数字则用单词buzz 代替。如果一个数字是3和5的公倍数,就用“FizzBuzz”。

用 for 表达式来输入游戏中1到100之间所有数字的正确答案。

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizBuzz "
    i % 3 == 0 -> "Fizz "
    i % 5 == 0 -> "Buzz"
    else -> "$i "
}

>>>  for (i in 1..100) {
         print(fizzBuzz(i))
     }
        
1 2 Fizz 4 Buzz Fizz 7 ...
           

变一下迭代规则,从100开始到1并且只输出偶数

>>>  for (i in 100 downTo 1 step 2) {
         printValue(fizzBuzz(i))
     }
           
  • downTo 就是倒序迭代。
  • setp 就是间隔距离,也可以说是 步长。

上面说到的 … 或者 downTo 区间表示方式都是包含结束值的,如果不想包含结束值的话可以使用 unit 函数来创建区间例如:

这就是一个从0到100不包含100的数列。

1.4.3 迭代map

看看如何打印字符二进制表示的小程序。

val binaryReps = TreeMap<Char, String>() //创建一个TreeMap
for (c in 'A'..'F') {  //使用字符区间迭代从A 到F 之间的字符
binaryReps[c] = Integer.toBinaryString(c.toInt()) //把ASCII码转换成二进制,同时根据键c把值存储到map中
}

for ((letter, binary) in binaryReps) {
    println("$letter = $binary")
}
           
  • … 语法不仅可以创建数字区间,还可以创建 字符区间 。这里就迭代了从’A’到’F’之间的所有字符,包括’F’。
  • 在for循环中允许展开迭代中的集合的元素(例子中展开的是map的键值对集合)。把展开的结果存储到了两个独立的变量中:letter是键,binary是值。
  • 其中还有一个使用的小技巧,根据键来访问和更新map的简明语法。使用map[Key]读取值,并使用map[kep] = value设置他们,不需要调用get和set。类似Java中数组的用法。

在平常开发中在迭代集合时,经常需要获取到当前项的下标。那么在Kotlin中该如何获取到集合的下标呢。

val list = arrayListOf("10", "11", "1001") //创建ArrayList对象。
for ((index, element) in list.withIndex()) { //withIndex() 根据下标来迭代
    println("$index:$element")
}
           

后面章节会深入了解 withIndex 的内容。

1.4.4 使用 “in” 检查集合和区间的成员

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z' //底层实现 就是 a<=c && c<=z || c<=A || c<=Z  在区间中,返回true
fun isNotDigit(c: Char) = c !in '0'..'9' // 不在这个区间返回true

>>>  println(isLetter('q'))
true
>>>  println(isNotDigit('x'))
true
```java
**in** 运算符和 **!in** 也适用于 **when** 表达式:
```Kotlin
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's a digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know..."
}

        
>>>  println(recognize('8'))
It's a digit!
           

区间并不局限于字符 。能够组成区间的对象底层实现了 java.lang.Comparable 接口。并不是所有的区间都能够迭代,例如就不能列举出“Java”和“Kotlin”之间所有的字符串。对于这种区间仍然可以使用 in 来检查一个对象是否属于这个区间:

>>>  println("Kotlin" in "Java".."Scala") //相当于 Kotlin<=Java && Scala<=Kotlin。  
//字符串是按照字母表顺序进行比较的。可以看String中Comparable接口的实现
true
           

in 同样适用于集合:

println("Kotlin" in setOf("Java", "Scala"))
false
           

1.5 Kotlin中的异常

Kotlin的异常处理和Java以及其他许多语言的处理方式都很类似。一个函数能够正常结束,也可以在出现错误的情况下抛出异常。方法的调用者能捕获这个异常并处理它;如果没有被处理,异常会沿着调用栈再次抛出。我们来看一段抛出异常的例子:

if (percentage !in 0..100) {
    throw IllegalArgumentException("A percentage value must be between 0 and 100:$percentage")
}
           
  • 不需要 new 关键词来创建实例
  • 在Kotlin中 throw 是一个表达式,可以当作另一个表达式的一部分来使用。

1.5.1 “try” “catch” 和 “finally”

直接看例子,从给定的文件中读取一行,尝试把它解析成一个数字,返回这个数字;或者当这一行不是一个有效数字时返回null。

fun readeNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)
    } catch (e: NumberFormatException) {
        return null
    } finally {
        reader.close()
    }
}
           
  • 和Java最大的区别在于这段代码没有捕获或者抛出IOException。在Java中IOException是一个 受检异常 ,这种异常必须显式的处理。必须声明你的函数能抛出的所有受检异常。如果调用另外一个函数,需要处理这个异常,或者声明你的函数也能抛出这个异常。
  • Kotlin不区分 受检异常 和 未受检异常 。这种设计是基于Java中的使用异常的实践做出的决定。经验显示这些Java规则常常导致许多毫无意义的重新抛出或者忽略异常的代码,而且这些规则不能总是保护你免受可能发生的错误。
  • 在上面代码中,NumberFormatException是一个 未受检异常 ,因此Java编译器并不会强迫你捕获它,但是在运行时很容易看到这个异常发生。而BufferedReader.close可能抛出需要处理的受检异常IOException。如果流关闭失败,大多数程序都不会采取什么有意义的行动,所以捕获来自close方法的异常所需的代码就是冗余的样板代码了。
在Java中异常分为两种:checkedExcepiton(受检异常)和uncheckedExcepiton(未受检异常)。未受检异常包括两种,一种是RuntimeException,另一种是Error。第一种比如说NullPointerException,ClassCastException等等,这种异常是不需要主动抛出或者捕获的。Error一般是系统层面上的异常,不需要捕获。

1.5.2 “try” 作为表达式

让我们修改一下上面的代码例子,看一下Kotlin和Java中关于异常的另一个显著的差异。

fun readeNumber(reader: BufferedReader) {
        val number = try {
            Integer.parseInt(reader.readLine()) //变成“try”表达式的值
        } catch (e: NumberFormatException) {
            return
        }
        println(number)
    }
        
>>>  val reader = BufferedReader(StringReader("not a number"))
>>>  readNumber(reader) //没有任何输出
           
  • Kotlin中的 try 关键字和 if 和 when 一样,引入一个表达式,可以把它的值赋给变量。
  • 在 catch 中直接 return 了,所有没有值输出,如果想要输出值,可以把 return 改成一个表达式或者一个变量。

总结

最后我们总结一下这几个章节的内容

  • fun 关键字用来声明函数。val 关键词和 var 关键字分别用来声明只读变量和可变变量
  • 字符串模板 帮助你避免繁琐的字符串连接。在变量名称前加上 ∗ ∗ 前 缀 或 者 用 ∗ ∗ ** 前缀或者用 ** ∗∗前缀或者用∗∗{ } 包围一个表达式,来把值注入到字符串中。
  • 值对象 类在Kotlin中以简洁的表达式。
  • 熟悉的 if 现在是带返回值的表达式。
  • when 表达式类似于Java中的 switch 但功能更强大。
  • 在检查过变量具有某种类型之后不必显式的转换它的类型:编译器使用 智能转换 自动帮你完成
  • for、while 和 do-while 循环与Java类似,但是 for 循环现在更加方便,特别是当你需要迭代map的时候,又或是迭代集合需要下标的时候。
  • 简洁的语法1…5会创建一个区间。区间和数列允许Kotlin在 for 循环中使用统一的语法和同一套抽象机制,并且还可以使用 in 运算符和 !in 运算符来检查值是否属于某个区间。
  • Kotlin中的异常处理和Java非常相似,除了Kotlin不要求你声明函数可以抛出的异常,try可以当作表达式。

下一章我们会讲 函数的定义与调用 。未完待续~