Kotlin语法基础

基本元素

函数

函数的声明以关键字fun开始,函数名称紧随其后,接下来是括号括起来的参数列表,之后是冒号隔开的返回类型,最后是大括号括起来的函数体。

fun 函数名(参数列表): 返回类型 {
    函数体
}
1
2
3
fun max(a: Int, b: Int): Int {
    return if (a > b) a else b // 与Java中的三元运算符相似
}
1
2
3

如果某个函数的函数体只有单个表达式,可用这个表达式作为完整的函数体,并去掉花括号和return语句。

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

因为表达式函数体的返回类型可以进行推导,所以可进一步简化,省掉返回类型。

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

语句和表达式

语句和表达式的区别是:表达式有值,并且能够作为另一个表达式的一部分使用;而语句总是包围着它的代码块中的顶层元素,并且没有自己的值。

在Java中,所有的控制结构都是语句。而在Kotlin中,除了循环(for、do和do/while)以外,大多数控制结构都是表达式。例如,if是表达式,而不是语句。

另外,Java中的赋值操作是表达式,而在Kotlin中变成了语句

变量

Kotlin的变量声明是以关键字开始,然后是变量名称,最后是类型。

val name: String = "daking"
1

可省略类型声明,Kotlin会根据初始化器来判断变量的类型。

val name = "daking"
1

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

  • val(来自value)—— 不可变引用。使用val声明的变量不能在初始化之后再次赋值。它对应的是Java的final变量。

  • var(来自variable)—— 可变引用。这种变量的值可以被改变。它对应的是普通(非final)的Java变量。

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

在定义val变量的代码块执行期间,val变量只能进行唯一一次初始化。但如果编译器能确保只有唯一一条初始化语句会被执行,可根据条件使用不同的值来初始化它。

val message: String
if (canPerformOperation()) {
    message = "success"
} else {
    message = "fail"
}
1
2
3
4
5
6

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

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

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

var result = 10
result = "xxx" // 错误,类型不匹配
1
2

字符串模板

Kotlin允许你在字符串字面值中引用局部变量,只需要在变量名称前加上字符$

String name = "daking";
String message = "I am " + name;
1
2
val name = "daking"
val message = "I am $name"
1
2

还可以通过${表达式}方式在字符串字面值中引用表达式。

val message = "I am ${people[0]}"
1

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

val message = "I am ${if (people.size > 0) people[0] else "someone" }"
1

as与is

使用as关键字来进行特定类型的显式转换。

val p: Person = Student()
val s: Student = p as Student
1
2

使用is检查一个变量是否为指定类型。

if (p is Student) {
    println("It is a student.")
}
1
2
3

Kotlin会进行智能转换:如果你检查过一个变量是某种类型,后面就不用再显式地转换它,可以直接把它当作你检查过的类型来使用。

if (p is Student) {
    p.study()
}
1
2
3

类和属性

一个只有一个属性的简单JavaBean类,对比下Java和Kotlin关于类的不同写法。

public class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
}
1
2
3
4
5
6
7
8
9
10
11
class Person(val name: String)
1

在Kotlin中实例化一个类对象,不需要new关键字。

val person = Person("daking")
1

属性

在Java中,数据存储在字段中,通常还是私有的。如果想让类的使用者访问到数据,得提供访问器方法:一个getter,可能还有一个setter。

在Java中,字段和其访问器的组合常常被叫作属性。而在Kotlin中,属性是头等的语言特性,完全代替了字段和访问器方法。

在Kotlin类中声明一个属性和声明一个变量一样:使用val或var关键字。声明成val的属性是只读的,而var属性是可读写的。

在Kotlin中,当你声明属性的时候,就声明了对应的访问器:val属性有一个getter,而var属性既有getter又有setter。访问器的默认实现非常简单:创建一个存储值的字段,以及返回值的getter和更新值的setter。若有需要,可声明自定义的访问器。

val person = Person("xxx")
person.name = "daking"
println(person.name)
1
2
3
Person person = new Person("xxx");
person.setName("daking");
System.out.println(person.getName());
1
2
3

getter和setter的命名规则有一个例外:如果属性的名称以is开头,getter不会增加任何的前缀;而它的setter名称中的is会被替换成set

class Person(
    val name: String,
    val isMarried: Boolean
)
1
2
3
4
val person = Person("xxx", false)
person.name = "daking"
println(person.name)
person.isMarried = true
println(person.isMarried)
1
2
3
4
5
Person person = new Person("xxx", false);
person.setName("daking");
System.out.println(person.getName());
(person.setMarried(true);
System.out.println(person.isMarried());
1
2
3
4
5

Kotlin源码布局

Java把所有的类组织成包。

每一个Kotlin文件都以一条package语句开头,而文件中定义的所有声明(类、函数及属性)都会被放在这个包中。

假设Kotlin文件A中定义的声明的包与文件B相同,那么文件A可以直接使用文件B中的定义;如果它们的包不相同,文件A需要导入文件B的对应定义。

package geometry.shapes // 包声明

import java.util.Random // 导入标准Java库的类

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

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}
1
2
3
4
5
6
7
8
9
10
11
12
13

和Java一样,导入语句放在文件的最前面并使用关键字import。Kotlin不区分导入的是类还是函数,而且它允许import任何种类的声明。

package geometry.example

import geometry.shapes.createRandomRectangle // 导入顶层函数

fun main(args: Array<String>) {
    println(createRandomRectangle().isSquare)
}
1
2
3
4
5
6
7

可以在包名称后加上*来导入特定包中定义的所有声明。注意这种导入不仅让包中定义的类可见,也会让顶层函数和属性可见。例如,上面例子中的导入语句可改为import geometry.shapes.*

在Java中,要把类放在和包结构相匹配的文件与目录结构中。

Java源码布局-w518

在Kotlin中,可以把多个类放在同一个文件中,文件的名称还可以随意选择。Kotlin也没有对磁盘上源码文件的布局强加任何限制。但遵循Java的目录布局并根据包结构把源码文件放到对应的目录中,是个良好的习惯。

Kotlin源码布局-w472

枚举和when

声明枚举类

使用enum class关键字来声明一个枚举类。

enum class Color {
    RED, GREEN, BLUE
}
1
2
3

和Java一样,枚举并不是值的列表,可以给枚举类声明属性和方法。注意要用分号把枚举常量列表和方法定义分开。

enum class Color(
    val r: Int, val g: Int, val b: Int
) {
    RED(255, 0, 0), GREEN(0, 255, 0), BLUE(0, 0, 255); // 注意此处的分号是必须的。
    
    fun rgb() = (r * 256 + g) * 256 + b
}
1
2
3
4
5
6
7
println(Color.RED.rgb())
1

使用when处理枚举类

Java中的分支判断语句switch在Kotlin对应的结构是when

fun getMnemonic(color: Color) =
    when(color) {
        Color.RED -> "Richard"
        Color.GREEN -> "Gave"
        Color.BLUE -> "Battle"
    }
1
2
3
4
5
6

和Java不一样,你不需要在每个分支都写上break语句,若匹配成功,只有对应的分支会被执行。

可以把多个值合并到同一分支,只需要用逗号隔开这些值。

fun getWarmth(color: Color) = when(color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.GREEN -> "neutral"
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}
1
2
3
4
5

when允许使用任意对象

Java中的switch要求必须使用常量(枚举常量、字符串或数字字面值)作为分支条件,而Kotlin的when允许使用任何对象。

fun mix(c1: Color, c2: Color) = 
    when(setOf(c1, c2)) {
        setOf(Color.RED, Color.YELLOW) -> ORANGE
        setOf(Color.BLUE, Color.YELLOW) -> GREEN
        setOf(Color.BLUE, Color.VIOLET) -> INDIGO
        else -> throw Exception("Dirty color") // 如果没有任何其他分支匹配,就会执行这里
    }
1
2
3
4
5
6
7

不带参数的when

如果没有给when表达式提供参数,分支条件就是任意的布尔表达式。

fun mixOptimized(c1: Color, c2: Color) = 
    when {
        (c1 == Color.RED && c2 == Color.YELLOW) ||
        (c1 == Color.YELLOW && c2 == Color.RED) ->
            ORANGE
            
        (c1 == Color.BLUE && c2 == Color.YELLOW) ||
        (c1 == Color.YELLOW && c2 == Color.BLUE) ->
            GREEN
            
        (c1 == Color.BLUE && c2 == Color.VIOLET) ||
        (c1 == Color.VIOLET && c2 == Color.BLUE) ->
            INDIGO
            
        else -> throw Exception("Dirty color")
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

用when代替if

Kotlin没有三元运算符,因为if表达式有返回值,可利用if (x) a else b来实现与三元运算符一样的功能。

可以使用when来代替连串的if表达式。

ifwhen都可以使用代码块作为分支体,代码块中的最后一个表达式就是结果。

迭代

while循环

Kotlin有while循环和do-while循环,它们的语法和Java中相应的循环没有什么区别。

while (condition) {
    /* ... */
}
1
2
3
do {
    /* ... */
} while (condition)
1
2
3

for循环

Kotlin中的for循环仅以唯一一种形式存在,和Java的for-each循环一致。写法为:for <item> in <elements>

区间和数列

区间本质上就是两个值之间的间隔,这两个值通常是数字,一个起始值,一个结束值。

使用..运算符来表示闭合区间(区间包含起始值和结束值)。例如,1..10'a'..'z'

如果你能迭代区间中所有的值,这样的区间被称为数列。例如,由1到10的整数构成的区间1..10

整数区间常与for循环一起使用。

for (i in 1..10) {
    println(i) // 循环打印出1到10的整数
}
1
2
3

可以使用until函数来创建半闭合区间(区间包含起始值,但不包含结束值)。例如,1 until 10

可以使用downTostep来创建一个带步长的数列。例如,10 downTo 1 step 2是一个从10到1的步长为2的数列。注意,step可以是正数或负数,从而实现递增或递减数列。

迭代map

for循环也常用于迭代集合。

for ((key, value) in map) { // 把键和值赋给两个变量
    println("$key = $value")
}
1
2
3

in运算符

使用in运算符来检查一个值是否在区间中,或者它的逆运算!in来检查这个值是否不在区间中。此做法同样适用于集合。

异常

一个函数可以正常结束,也可以在出现错误的情况下抛出异常。方法的调用者能捕获到这个异常并处理它;如果没有被处理,异常会沿着调用栈再次抛出。

抛出异常

Kotlin中抛出异常的方式与Java类似。

if (percentage !in 0..100) {
    throw IllegalArgumentException("A percentage value must be between 0 and 100.")
}
1
2
3

和Java不同的是,Kotlin中的throw结构是一个表达式,能作为另一个表达式的一部分使用。

val percentage = 
    if (number in 0..100)
        number
    else
        throw IllegalArgumentException("A percentage value must be between 0 and 100.")
1
2
3
4
5

处理异常

和Java一样,Kotlin也是使用带有catchfinally子句的try结构来处理异常。

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLines()
        return Integer.parseInt(line)
    } catch (e: NumberFormatException) {
        return null
    } finally {
        reader.close()
    }
}
1
2
3
4
5
6
7
8
9
10

如果用Java来写这个函数,要在函数声明后写上throws IOException。这是因为IOException是一个受检异常。在Java中,受检异常必须显式地处理,必须为函数声明能抛出的所有受检异常。

Kotlin并不区分受检异常和未受检异常。不用指定函数抛出的异常,而且可以处理也可以不处理异常。

try可以作为表达式来使用,它的代码块中的最后一句就是表达式结果。catch也是。

fun readNumber(reader: BufferedReader): Int? {
    return try {
        val line = reader.readLines()
        Integer.parseInt(line)
    } catch (e: NumberFormatException) {
        null
    }
}
1
2
3
4
5
6
7
8