Kotlin 开篇之基础语法篇

文章目录

  • 前言
  • Kotlin 基础
    • 1. 函数声明
      • 1.1 表达式函数体
      • 1.2 函数调用
    • 2. 变量
      • 2.1 可变变量和不可变量
      • 2.2 引用变量值
      • 2.3 空安全
    • 3. 基础类型
      • 3.1 数字类型
      • 3.2 布尔类型
      • 3.3 字符类型
      • 3.4 字符串类型
      • 3.5 数组类型
    • 4. 流程控制
      • 4.1 if
      • 4.2 when
      • 4.3 while 和 for 循环
    • 5. 类、枚举以及属性
      • 5.1 类
      • 5.2 枚举类
    • 6. 智能转换
    • 7. 异常处理
      • 7.1 异常捕获、处理
      • 7.2 自定义异常
  • 总结
  • 参考


前言

Kotlin 语言由程序语言开发工具的知名供应商 JetBrains 构思于 2010 年,它是一种针对 Java 平台的新编程语言 (基于 JVM 的语言)。Kotlin 简洁、安全、务实,并且专注于与 Java 代码的互操作性。它几乎可以用在现在 Java 使用的任何地方:服务端开发、Android 应用等等。Kotlin 可以很好地和所有现存的 Java 库和框架一起工作,且性能水平和 Java 旗鼓相当,同时作为一种新语言,它包含了许多新的特性,由此也决定着 Kotlin 的代码风格。本文先来学习下 Kotlin 的基础语法,包括变量、函数、流程控制和智能转换等,这些基础语法是程序最基本的元素。


Kotlin 基础

本节来学习怎样用 Kotlin 声明开发过程中经常使用的一些程序最基本的要素:函数声明、变量、基础类型、流程控制、类、枚举以及属性、智能转换和异常处理等。

先来回顾一段经典代码,还记得刚开始学 Java 时怎么打印输出 Hello World ! 的吗?下面使用 Kotlin 语言来实现,代码如下:

fun main(args: Array<String>) {
    println("Hello World!")
}

这么简洁?是的相比于 Java 代码的实现,Kotlin 就是这么简洁。运行这段代码,最快的方式就是使用 Kotlin 官方提供的在线工具 Playground。不过还是推荐使用 IntelliJ IDEA,这是 Kotlin 官方提供的集成开发工具,也是世界上最好的 IDE 之一。

1. 函数声明

Kotlin 中函数的声明与 Java 有所不同,来看一段示例代码:

/*
关键字  函数名称     参数类型  返回值类型
 ↓       ↓            ↓        ↓     */
fun helloFun(name: String): String {
    return "Hello $name !"
}/*    ↑
   花括号内为:函数体 - 代码块体
*/
  • 关键字 fun 用来声明一个函数。
  • 函数名称,与 Java 类似使用驼峰命名法 (大部分情况下)。
  • 函数参数,是以 (name: String) 这样的形式传递的,参数的类型写在参数名称的后面 (稍后变量的声明也是这样),即参数 name 的类型为 String 类型。
  • 函数可以定义在文件的最外层,不需要把它放在类中。
  • 函数的返回值类型跟在参数列表的后面,它们之间用冒号隔开。
  • 和许多其它现代语言一样,可以省略每行代码结尾的分号。

1.1 表达式函数体

上面的函数体是由单个表达式构成的,因此可以用这个表达式作为完整的函数体,并去掉花括号和 return 语句,直接使用 = 连接,将其变成一种类似变量赋值的函数形式,简化后代码如下:

/*
关键字  函数名称     参数类型  返回值类型    表达式体
 ↓       ↓            ↓        ↓           ↓      */
fun helloFun(name: String): String = "Hello $name !"

如果函数体写在花括号中,则表示这个函数有代码块体。如果函数直接返回了一个表达式,则表示其有表达式体,也称其为单一表达式函数

此外,由于 Kotlin 支持类型推导,在使用单一表达式函数形式的时候,返回值的类型也可以省略,简化后代码如下:

/*
关键字  函数名称     参数类型      表达式体
 ↓       ↓            ↓           ↓      */
fun helloFun(name: String) = "Hello $name !"

注意:只有表达式体函数的返回类型可以省略,对于有返回值的代码块体函数,必须显式地写出返回类型和 return 语句。

1.2 函数调用

Kotlin 的优势不仅仅体现在函数声明上,在函数调用的地方,也有很多独到之处,如果要调用上面声明的函数,可以通过代码:

helloFun("Kotlin")

不过,Kotlin 提供了一些新的特性,如:命名参数,简单理解就是,它允许在调用函数的时候传入形参的名称。

helloFun(name = "Kotlin")

来看一个更具体、更复杂的函数调用,函数声明如下:

fun createUser(
    name: String,
    age: Int,
    gender: Int,
    address: String,
    feedCount: Int,
    likeCount: Long,
    commentCount: Int
) {
    //..
}

声明一个包含了很多参数的函数,在 Kotlin 中,针对参数较多的函数,一般会以纵向的方式排列,这样的代码更符合我们从上到下的阅读习惯,省去从左往右翻的麻烦。

有了命名参数,可以这样来调用函数:

createUser(
    name = "Tom",
    age = 30,
    gender = 1,
    address = "Kotlin",
    feedCount = 2093,
    likeCount = 10937,
    commentCount = 3285
)

把函数的形参和实参用 = 连接,建立两者的对应关系,使得代码的可读性更强,如果将来要修改 likeCount 这个参数,也是一目了然,很方便就能定位到,体现了代码的易维护性 (参数较多的时候,Java 代码中需按参数顺序来查找并修改)。

此外,Kotlin 还支持参数默认值,这个特性在参数较多的情况下同样有很大的优势。

fun createUser(
    name: String,
    age: Int,
    gender: Int = 1,
    address = "Kotlin",
    feedCount: Int = 0,
    likeCount: Long = 0L,
    commentCount: Int = 0
) {
    //..
}

在函数的参数列表中,gender、address 等参数都被赋予了默认值,这样的好处是在调用的时候可以简化函数的入参,代码如下:

createUser(
    name = "Tom",
    age = 30,
    commentCount = 3285
)

在调用函数时,只传了 3 个参数,剩余的参数没有传,但是 Kotlin 编译器会自动填充上默认值。对于无默认值的参数,编译器会强制要求在调用时必须传参。对于有默认值的参数,则可传可不传 (有助于提升开发效率)。

2. 变量

在学习 Java 时,如果我们要声明变量,必须要声明它的类型,后面跟着变量的名称和对应的值,然后以分号结尾,代码如下:

// 定义变量 age,其类型为 Integer,值为 18
Integer age = 18;

Kotlin 则不一样,因为许多变量声明的类型都可以省略,因此在 Kotlin 中以关键字开始,然后是变量名称,最后可以加上类型,代码如下:

/*
关键字     变量类型
 ↓          ↓           */
var price: Int = 100;   /*
      ↑           ↑
   变量名称      变量值   */

和表达式函数一样,由于 Kotlin 支持类型推导,如果不指定变量的类型,编译器会分析初始化器表达式的值,并把它的类型作为变量的类型。注意:上面代码末尾的分号可以省略。简化后代码如下:

/*
关键字   变量类型默认推导为 Int
 ↓                 */
var price = 100    /*
      ↑      ↑
   变量名称  变量值  */

2.1 可变变量和不可变量

声明变量的关键字有两个:

  • val:取自 value,表示不可变引用,使用 val 声明的变量不能在初始化之后再次赋值。其对应的是 Java 的 final 变量。
  • var:取自 variable,表示可变引用,使用 var 声明的变量其值可以被改变。其对应是普通的 Java 变量 (非 final)。

默认情况下,应该尽可能地使用 val 关键字来声明所有的 Kotlin 变量,仅在必要的时候使用 var 关键字来声明变量。

注意:尽管 val 引用自身是不可变的,但是它指向的对象可能是可变的。

/*
关键字  声明不可变引用 
 ↓       ↓           */
val languages = arrayListOf("Java")
languages.add("Kotlin")  // 改变引用指向的对象

注意:即使 var 关键字允许变量改变自己的值,但是它的类型却是改变不了的。

var answer = 42
answer = "no answer"  // 错误:类型不匹配

提示:如果需要在变量中存储不匹配类型的值,必须手动把值转换或强制转换到正确的类型。

2.2 引用变量值

声明变量后,自然是要使用的,下面看一下怎么使用变量的值,代码如下:

fun main(args: Array<String>) {
    val name = "Kotlin"
    println("Hello $name !")  // 打印输出“Hello Kotlin !”
}

在代码中,声明一个变量 name,并在后面的字符串字面值中使用。和许多脚本语言一样,Kotlin 可以在字符串字面值中引用局部变量,只需要在变量名称前面加上字符 $。这等价于 Java 中使用 + 来拼接字符串,效率一样但是更紧凑。注意

  • 表达式会进行静态检查,如果引用一个不存在的变量,代码则根本通不过编译。
  • 如果要在字符串中使用 $ 字符,需要对它做转义。

Kotlin 还可以引用更复杂的表达式,不仅限于简单的变量名称,只需要把表达式用花括号括起来即可,代码如下:

fun main(args: Array<String>) {
    if (args.isNotEmpty()) {
        println("Hello ${args[0]} !")  // 使用 ${} 的语法插入 args 数组中的第一个元素
    }
}

此外:还可以在双引号中直接嵌套双引号,只要它们处在某个表达式的范围内 (即花括号内)。

fun main(args: Array<String>) {
    println("Hello, ${if (args.isNotEmpty()) args[0] else "someone"}!")
}

2.3 空安全

在 Kotlin 中一切皆是对象,因此对象就有可能为空,那么可不可以给一个变量赋空值呢?参考下面代码:

val score: Double = null  // 编译器报错:null 不能赋值给 Double 类型的非空变量

由于 Kotlin 强制要求在定义变量的时候,需指定这个变量是否可能为 null,对于可能为 null 的变量,在声明的时候要在变量类型后面加一个问号 “?”,代码如下:

val score: Double? = null  // 编译通过

注意:Kotlin 对可能为空的变量类型做了强制区分,即可能为空的变量无法直接赋值给不可为空的变量。不过,反向赋值是可以的。

var grade: Double? = null
var score: Double = 148.toDouble()

grade = score  // 编译器报错
score = grade  // 编译通过

3. 基础类型

在 Java 中,基础类型分为原始类型 (Primitive Type) 和包装类型 (Wrapper Type)。如:整型会有对应的 intInteger,前者是原始类型,后者是包装类型。

Java 这样设计,是因为原始类型的开销小、性能高,但它不是对象,无法很好地融入到面向对象的系统中。而包装类型的开销大、性能相对较差,但它是对象,有成员变量以及成员方法,可以很好地发挥面向对象的特性。

Kotlin 中的基础类型,包括:数字类型、布尔类型、字符类型、及由前面这些类组成的数组等。在 Kotlin 语言体系中,没有原始类型这个概念,也就是在 Kotlin 中一切皆是对象。看一段代码:

/*
关键字 变量名 变量类型   调用 148 的成员方法
 ↓     ↓      ↓          ↓         */
val score: Double = 148.toDouble()

这里由于整型数字 “148” 被看作是对象,因此可以调用它的成员方法 toDouble(),这在 Java 中是不允许的。

虽然 Kotlin 在语法层面摒弃了原始类型,但有时候为了性能考虑,我们确实需要用原始类型?那么可以使用非空原始类型,编译器会自动编译成 Java 的原始类型。

3.1 数字类型

在数字类型上,Kotlin 和 Java 几乎是一致的,包括它们对数字“字面量”的定义方式。通过一段代码及注释来介绍:

val int = 1				// 整数默认会被推导为 Int 类型
val long = 1234567L		// Long 类型需要使用 L 后缀
val double = 13.14		// 小数默认会被推导为 Double 类型,但不需要使用 D 后缀
val float = 13.14F		// Float 类型需要使用 F 后缀
val hexadecimal = 0xAF	// 使用 0x 前缀代表十六进制字面量
val binary = 0b01010101 // 使用 0b 前缀代表二进制字面量

对于数字类型的转换,Kotlin 与 Java 的转换行为是不一样的。Java 可以隐式转换数字类型,而 Kotlin 更推崇显式转换。比如,在 Java 中经常直接把 int 类型的值赋值给 long 类型的变量,此时编译器会自动做类型转换。但需注意的是:不同类型数据之间的互相转换是存在精度问题的,尤其是当这样的转换代码掺杂在复杂的逻辑中时,在碰到一些边界条件的情况下,即使出现 Bug 也不容易排查出来。

int i = 100;
long j = i;  // 编译不会报错,存在精度问题

同样的代码,在 Kotlin 中是行不通的,代码如下:

val i = 100
val j: Long = i  // 编译器报错 - 类型不匹配

在 Kotlin 中,抛弃了隐式转换,推崇使用显示转换,即调用 Int 类型的 toLong() 函数进行转换后赋值:

val i = 100
val j: Long = i.toLong()  // 编译通过,Kotlin 提供了很多类似的函数

通过这种显式的转换,使得代码的可读性更强,同时代码也更容易维护。

3.2 布尔类型

布尔类型用 Boolean 来表示,该类型只有两种值分别是 truefalse。布尔类型支持一些逻辑操作:

  • “&”:表示“与运算”。
  • “|”:表示“或运算”。
  • “!”:表示“非运算”。
  • “&&”和“||”:分别表示它们对应的“短路逻辑运算”。

3.3 字符类型

字符类型用 Char 来表示字母 (大写和小写)、数字和其它符号,每个字符只是一个符号,包含在单引号中。

val c: Char = 'A'
val cha: Char = 'a'
val money: Char = '¥'

3.4 字符串类型

字符串类型用 String 来表示,字符串,顾名思义,就是一连串的字符序列,在大部分情况下,使用双引号来表示字符串的字面量。注意:和 Java 一样,Kotlin 中的字符串也是不可变的。

val str = "Hello Kotlin!"

此外,Kotlin 还新增了一个原始字符串,是用三个双引号来表示其字面量的。可以用于存放复杂的多行文本,并且它定义的时候是什么格式,最终打印也会是对应的格式。所以当我们需要复杂文本的时候,就不需要像 Java 那样写一堆的加号和换行符。

val str = """
       当我们的字符串有复杂的格式时
       原始字符串非常的方便
       因为它可以做到所见即所得。"""

3.5 数组类型

Kotlin 中的数组与 Java 相比有一些改变,在 Kotlin 中,一般使用 arrayOf() 函数来创建数组,括号当中可以用于传递数组元素进行初始化,同时 Kotlin 编译器也会根据传入的数组元素进行类型推导。

val arrayInt = arrayOf(1, 2, 3)	 // 类型推导为 整形数组
val arrayString = arrayOf("Java", "Kotlin")  // 类型推导为 字符串数组

在 Java 中,数组和其它集合的操作是不一样的,如获取数组的长度,使用 Array # length() 方法,如果获取集合 List 的大小,则使用 List # size() 方法,这主要是因为数组不属于 Java 集合。

而 Kotlin 的数组虽然也不属于集合,但它的一些操作是跟集合统一的,比如获取数组的长度,使用的是 Array # size() 方法,示例代码如下:

val array = arrayOf("Java", "Kotlin")
println("Size is ${array.size}")
println("First element is ${array[0]}")

4. 流程控制

Kotlin 中,流程控制主要有 ifwhenwhilefor,使用它们可以控制代码的执行流程,也是体现代码逻辑的关键。

4.1 if

在程序开发中 if 语句主要是用于逻辑判断,代码如下:

val i = 1
if (i > 0) {  // 如果变量 i 的值大于0则打印 Big 否则打印 Small
    print("Big")
} else {
    print("Small")
}

此外,Kotlin 的 if,并不是程序语句那么简单,它还可以作为表达式来使用。

val i = 1
val message = if (i > 0) "Big" else "Small"

代码中把 if 当作表达式,并将 if 判断的结果赋值给变量 message,同时 Kotlin 编译器会根据 if 表达式的结果自动推导出变量 message 的类型为 String,使得代码更加简洁。

另外,Kotlin 还提供了一种简写,叫做 Elvis 表达式,示例代码如下:

fun getLength(text: String?): Int {
  return text?.length ?: 0
}

通过 Elvis 表达式,针对可空变量,不必再写 “if (xxx != null) xxx else xxx” 这样的代码逻辑,提高代码可读性和编码效率。

4.2 when

if 相似,Kotlin 中的 when 主要也是用于逻辑判断的,它可以被认为是 Java 中的 switch case,但是又比 switch case 更强大,使用的也更频繁。

val i: Int = 1
when(i) {
    1 -> print("一")
    2 -> print("二")
    3 -> print("三")
    else -> print("其它")
}

示例代码可以看到,确实跟 Java 的 switch case 语句很像,但是比 switch case 强在哪里呢?其实跟上面的 if 一样,when 语句也可以作为表达式为变量赋值,代码如下:

val i: Int = 1
val message = when(i) {
    1 -> "一"
    2 -> "二"
    else -> "其它" // 如果去掉这行,会报错
}

注意

  • 与 switch case 不一样的是,when 表达式要求它里面的逻辑分支必须是完整的。即上面的示例代码,如果去掉 else 分支编译期会报错,因为 1 和 2 两个分支并没有覆盖所有的情况。
  • Java 中的 switch case 要求必须使用常量 (枚举常量、字符串或者数字字面值) 作为分支条件,而 Kotlin 中的 when 允许使用任何对象作为分支条件。
  • 如果没有给 when 表达式提供参数,分支条件就是任意的布尔表达式,这种写法的优点是不会创建额外的对象,但代价是较难理解和维护。

4.3 while 和 for 循环

Kotlin 有 whiledo-while 循环,它们的语法和 Java 中相应的循环没有什么区别,一般用于重复执行某些代码。

var i = 0
while (i <= 2) { // 如果 i 值小于等于 2 则输出并自增 1
    println(i)
    i++
}

var j = 0
do {
    println(j)
    j++
} while (j <= 2)

而对于 for 语句,Kotlin 不是常规的先初始化变量,然后在循环的每一步更新它的值,并在值满足某个限制条件时退出循环。而是使用了区间的概念,区间本质上就是两个值之间的间隔,这两个值通常是数字:一个起始值,一个结束值。使用 运算符来表示区间:

val oneToTen = 1..10  // 表示 [1, 10] 左闭右闭
val aToG = 'A'..'G' // 表示 [A, G] 左闭右闭

注意:Kotlin 的区间是包含的或者闭合的,意味着第二个值始终是区间的一部分。

接下来,使用 for 语句来对上面的闭区间进行迭代:

for (i in oneToTen) { // 正序输出 1..10 的值
    println(i)
}

这里不仅可以正序迭代输出,还可以逆序迭代输出:

for (i in 10 downTo 0 step 2) { // 逆序迭代,从 10 到 0,迭代步长为 2
    println(i) // 输出 10 8 6 4 2 0
}

注意:逆序区间我们不能使用 6…0 来定义 (区间:结束值大于等于起始值),如果用这样的方式来定义的话,代码将无法正常运行。

还有,针对区间可以使用 in 运算符来检查一个值是否在区间中,或者它的逆运算 !in 来检查这个值是否不在区间中。

val aToG = 'A'..'G' // 代表 [A, G]
println('B' in aToG) // ‘B’ 在区间内,输出 true
println('H' !in aToG) // ‘H’ 不在区间内,输出 true

5. 类、枚举以及属性

不论 Java 还是 Kotlin 中,类可以将其理解为对某种事物的抽象模型

在 Java 中,Object 类是所有类的父类,即 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法。

在 Kotlin 中,其所有类都有一个共同的超类 Any,这对于没有超类型声明的类是默认超类,Any 类:它除了 equals() 、 hashCode() 和 toString() 外没有任何成员。

5.1 类

Kotlin 中定义类有些地方不同于 Java,在 Kotlin 中类是由 class 声明,其类声明有三部分:类名、类头 (指定类型参数、主构造函数)、类体 (花括号),其中类头和类体都是可选。下面通过一段代码及注释来详解:

/*
关键字  类名 可见性修饰符  主构造方法关键字声明
 ↓      ↓       ↓          ↓                      */
class Person private constructor(userName: String) {
    private var mUserName: String
    private var mAge: Int
    private var mAddress: String = ""
    init {	// init 代码块在主构造器被调用时调用
        println("init Person")
        // 可以直接使用主构造方法中定义的参数
        mUserName = userName
        // 也可以用于给属性赋初值
        mAge = 0
    }
	// 次要构造方法,非必须的,如果有,那么可以有多个
    constructor(userName: String, age: Int) : this(userName) { // 直接调用主构造方法
        mAge = age
    }
	// 次要构造方法,间接调用主构造方法,在同一个类中代理另一个构造函数使用 this 关键字
    constructor(userName: String, age: Int, address: String) : this(userName, age) {
        mAddress = address
    }
}

// 实例化对象时,直接调用构造方法即可,由于声明时加了 private 访问修饰符,在外部实例化会报错
var people = Person("Chris")
// 空实现类可以不需要花括号 {}
class Man
  • Kotlin 声明一个类默认就是 public 的,不用显示地声明为 public,且默认是 final 的、不可继承的,如果这个类允许被继承,必须使用 open 关键字修饰该类。
  • Kotlin 规定每个类最多有一个主构造方法和多个次要构造方法,次要构造方法不是必须的,如果有,那么可以有多个。
  • 主构造方法定义在类名之后,用 constructor 关键字声明,属于类头的一部分。注意:如果主构造方法没有任何注解修饰或者可见性修饰符,则 constructor 可以省略。
  • 某些情况下,可能不想让一个类能够从外部实例化,那么你可以让主构造函数用 private 修饰符来限制访问 (这时 constructor 不可省略)。
  • 次要构造方法是放在类体内定义的 (而非类头),且次要构造方法没有方法名,只有关键字 constructor (不可省略)。注意:Kotlin 中的次要构造方法必须使用 this 关键字,直接或间接地委托给主构造方法,主构造方法先调用,然后才是次要构造方法体内的代码。
  • 如果一个非抽象类,没有定义任何构造方法,编译器会自动提供一个默认的主构造方法 (无参构造方法),访问级别 public。如果不想声明的类有公共的构造函数,就得声明一个空的主构造函数,并用 private 修饰符来限制访问。
  • JVM 虚拟机中,如果主构造方法的所有参数都提供了默认值,那么编译器会自动创建一个附加的无参构造方法,同时这个无参构造方法会使用所提供的默认值初始化。
  • Kotlin 中主构造方法中没有代码 (没有方法体),其方法体在类体内的 init 代码块中,init 代码块中的代码会在主构造方法被调用时调用,并且构造参数可用于 init 和属性初始化。相当于将主构造方法的方法头和方法体分开定义,方法头放在类头后面。
  • 没有 new 关键字,实例化对象时,直接调用构造方法即可。
  • 如果一个类是空实现,可以不需要花括号 {}

Kotlin 的可见性修饰符与 Java 类似,但是默认的可见性修饰符不一样,如果省略修饰符:Java 默认包私有,Kotlin 默认声明是 public 的。

修饰符 类成员 顶层声明
public (默认) 所有地方可见 所有地方可见
internal 模块中可见 模块中可见
protected 子类中可见
private 类中可见 文件中可见

internal 只在模块内部可见。一个模块就是一组一起编译的 Kotlin 文件,这可能是一个 intellij IDEA 模块,一个 Eclipse 项目,或者一组使用调用 ant 任务进行编译的文件。

此外,Kotlin 还提供一种简化的构造方法定义形式,在定义构造参数的同时就对属性进行初始化 (可以是 var 或 val)。

/*
关键字   类名  访问修饰符  主构造方法中对属性进行初始化
  ↓      ↓       ↓                ↓                         */
class Person (private val username: String, private val age:Int) {
	......
}

类的成员变量或属性直接在主构造方法中定义,代替了构造参数。它们和构造参数的区别在于,作用域不同。在主构造方法定义的属性,其在整个类的作用域内有效,而构造参数仅在 init 代码块中有效,或者在定义属性初始化值时有效。

最后,对于类中的属性,Kotlin 编译器会根据实际情况,自动为其生成 gettersetter

  • getter:获取该属性值的方法,还可以通过自定义 get() 方法改变返回值的规则。
  • setter:对该属性进行的赋值的方法,想要改变属性的赋值逻辑时,还可以通过自定义 set() 方法来实现。

注意:Kotlin 的属性要求必须明确提供初值,否则要么修改为 optional,要么使用 lateinit 关键字延迟初始化。

lateinit 关键字

  1. 该修饰符只能用于在类体中 (不是在主构造函数中) 声明的 var 属性。
  2. 并且仅当该属性没有自定义 getter 或 setter 时。
  3. 该属性必须是非空类型,并且不能是原生类型。

5.2 枚举类

枚举类最基本的用法是实现一个类型安全的枚举,枚举常量用逗号分隔,每个枚举常量都是一个对象。

/*
软关键字 关键字 类名
  ↓      ↓    ↓  */
enum class Color {
    RED, BLACK, BLUE, GREEN, WHITE
}

每一个枚举都是枚举类的实例,它们可以被初始化,即创建每个枚举常量时可以指定其属性值:

enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF); // 必须加分号

	fun rgb() = { ... } // 枚举类定义一个方法
}

注意:如果要在枚举类中定义任何方法,要使用分号 ; 将枚举常量列表和方法定义分隔开。

6. 智能转换

// Expr 接口没有声明任何方法,它只是一个标记接口
// 用来给不同种类的表达式提供一个公共的类型
interface Expr

// 声明类的时候,使用一个冒号(:)+接口名称,来标记这个类实现了这个接口
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

fun eval(e: Expr): Int {
    if (e is Num) {
        // 使用 as 关键字来表示到特定类型的显示转换
        val num = e as Num //显示地转换成类型 Num 是多余的
        return num.value
    }
    if (e is Sum) {
        //变量 e 被智能地转换为类型 Sum,因此可以直接使用 e.left 和 e.right
        return eval(e.left) + eval(e.right)
    }
    return 0
}

Java 中,通过 instanceof 关键字来判断一个对象是否是某种类型,在使用 instanceof 关键字判断之后,还需显式地加上类型转换。如果最初的变量会使用超过一次,常常选择把类型转换的结果存储在另一个单独的变量中。

而在 Kotlin 中,使用 is 检查来判断一个对象是否是某种类型。不同的是,在 Kotlin 中,编译器已经帮我们执行了类型转换,不用再手动添加类型转换,我们把这种行为称为智能转换

注意:智能转换只在变量经过 is 检查且之后不再发生变化的情况下有效。当你对一个类的属性进行智能转换时,这个属性必须是一个 val 属性,而且不能有自定义的访问器。否则,每次对属性的访问是否都能返回同样的值将无从验证。

7. 异常处理

异常是在程序运行时可能发生的不必要的问题,并突然终止程序。异常处理是一个过程,使用它可以防止程序出现可能破坏代码的异常,有两种类型的异常:

  • 受检查异常:受检查异常声明为方法签名的一部分,并在编译时检查,例如 IOException。受检查异常要么用 try-catch 捕获,要么抛出,否则会发生编译错误。
  • 运行时异常:又叫非受检查的异常,不需要作为方法的一部分添加签名,需在运行时检查它们,例如 NullPointerException

注意:在 Kotlin 中,并不区分受检查异常和运行时异常,所有异常都是运行时异常,即便是原本在 Java 中的受检查异常,在 Kotlin 中也是运行时异常,如:IOException 在 Java 中是受检查异常,在 Kotlin 中是运行时异常。

Kotlin 的异常有 3 种:ExceptionErrorThrowable。它们的继承关系如下:
Kotlin 开篇之基础语法篇_第1张图片

Kotlin 异常类继承关系
 

Throwable:异常都直接或间接的继承于该类,在 Throwable 类中有几个非常重要的属性和函数:

  • message 属性:保存发生错误或异常时的详细信息。
  • printStackTrace 函数:打印错误或异常堆栈跟踪信息。
  • toString 函数:获取错误或异常对象的描述。

Error:程序无法恢复的严重错误,只能让程序中止。例如:Java 虚拟机内部错误、内存溢出和资源耗尽等严重情况。

Exception:程序可以恢复的异常,属于可控范围内的。例如:除零异常、空指针访问、网络连接中断和读取不存在文件等。

7.1 异常捕获、处理

处理 Kotlin 中的异常与 Java 相同,使用 trycatchfinally 块来处理代码中的异常。

fun main(args: Array<String>) {
    try {
        var num = 10 / 0 // 被除数不可以为 0 因此会抛出异常
        // 使用 throw 关键字抛出异常,异常执行前的语句已执行
        // 但异常后的语句未执行,因为控制流已转移到catch块
        throw Exception("Something went wrong here")
        println(num)
    } catch (e: ArithmeticException) {
    	// 捕获算术异常并打印输出
        println("Arithmetic Exception")
    } catch (e: Exception) {
    	// 捕获其它异常并打印输出
        println(e.message)
    } finally {
    	// 无论是否发生异常,始终会执行 finally 块
        println("It will print in any case.")
    }
}
  • try、catch、finally 三个语句块均不能单独使用,三者可以组成 try…catch…finally、try…catch、try…finally 三种结构。
  • try 表达式中可以有 0 个或多个 catch 代码块,最多一个 finally 代码块,但是,catch 或 finally 代码块至少要出现一个与 try 配对出现。
  • 不论在 try 代码块、catch 代码块中执行怎样的代码 (除非退出虚拟机 System.exit(1)),否则只要有 finally 代码块就总会被执行。
  • try、catch、finally 三个代码块中变量的作用域为代码块内部,分别独立而不能相互访问。如果要在三个代码块中都可以访问,则需要将变量定义到这些块的外面。
  • 多个 catch 块时候,只会匹配其中一个异常类并执行 catch 块代码,而不会再执行别的 catch 块,并且匹配 catch 语句的顺序是由上到下。
  • 使用 throw 关键字抛出异常,此时异常前的语句已执行,但异常后的语句未执行,因为控制流已转移到 catch 代码块。注意:和所有其它类一样,不必使用 new 关键字来创建异常实例。

7.2 自定义异常

在 Kotlin 标准库中封装的异常类型,不可能会预见所有的可能碰见的异常情况,此时自己定义异常,来表示程序中可能出现的特定问题。

而如果要自定义异常,就必须继承现有的异常类,一般都继承其异常情况相似的类,建立异常类型最简单的方法就是使用编辑器产生默认的构造方法,这样简单而有效。

class CustomException : Exception {
    // 无参构造
    constructor() {}

    // 带参构造
    constructor(msg: String) : super(msg) {}
}

fun throwCustomExFun(param: String?) {
    if (param == null) {
 		// 使用 throw 抛出自定义异常
        throw CustomException("param is null")
    }
}

注意:在 Kotlin 中不要求显式地指定函数可能抛出的异常,并且可以处理也可以不处理异常。


总结

Kotlin 和 Java 的语言很相似,但又有一些改进和优化之处。本文介绍了一些 Kotlin 的基础知识:

  • 基础类型不支持隐式类型转换,可规避一些隐藏问题,如精度问题。
  • 支持类型推导、代码末尾不需要分号。
  • 推崇不可变性 val,对于没有修改需求的变量尽量使用 val
  • 提供字符串模版、原始字符串支持复杂文本格式。
  • 单一表达式函数,简洁且符合直觉。
  • 函数参数支持默认值,替代 Builder 模式的同时,增强了代码可读性。
  • if 和 when 可以作为表达式,when 表达式,强制要求逻辑分支完整,让你写出来的逻辑永远不会有漏洞。
  • 强制区分可为空变量类型不可为空变量类型,规避空指针异常。
  • 数组访问行为与集合统一。
  • 函数调用支持命名参数,提高可读性,降低代码的维护成本。

参考

  1. 极客时间-朱涛 · Kotlin 编程第一课
  2. Kotlin 实战

你可能感兴趣的:(Kotlin,kotlin)