Kotlin 基础

主要内容

  1. 一个基本Kotlin例子
  2. Kotlin语言的主要特质(traits)
  3. Android和服务端开发的可能性
  4. Kotlin和其它语言的不同
  5. 编写和运行Kotlin代码

Basic elements: functions and variables

本章节介绍构成每个Kotlin程序的基本要素:函数和变量。你将会看到Kotlin如何让你省略了许多类型声明,如何促使你使用不可变数据(immutable),而不是可变数据(mutable)。

Hello, world!

从"Hello World"开始:

1
2
3
fun main(args: Array<String>) {
println("Hello world!")
}

从这个简单的代码片段可以一瞥语言的部分特性(feature):

  • fun关键字用于声明函数。
  • 参数类型写在参数名之后。变量的声明也如此。
  • 函数可以声明在一个文件的最顶层;你不需要将其放置在一个类内声明。
  • Kotlin中的数组就是类。和Java不同,Kotlin没有对数组类型的特定语法。
  • 使用 println 而不是 System.out.println。Kotlin标准库使用简洁的语法,提供了许多关于Java库函数的包装(wrappers),println就是其中之一。
  • 和其它现代语言一样,你可以在行末省略分号(; )。

Functions

如果希望函数包含返回类型,如何在签名中定义?

1
2
3
4
5
6
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}

>>> println(max(1,2))
2

函数的声明由fun关键字开始,随后是函数名,例如这里的max。之后是括号内的参数列表。返回类型跟随在参数列表之后,由冒号分隔。

图Figure 2.1 展示了一个函数的基本结构。注意在Kotlin,if是一个带有结果的表达式。类似于Java的三元操作:(a > b) ? a : b

Figure 2.1

Statements and expressions

在Kotlin,if是一个表达式,不是语句。区分于语句和表达式的不同点在于,表达式是一个值,它可以被用作另一个表达式的一部分,而一个语句在它的封闭块内总是一个顶层元素,而且没有自己的值。在Java中,所有控制结构(if-else, for, etc)都是语句。而在Kotlin中,除了循环(for, do, do/while)控制结构,其它都是表达式。这种可以组合控制结构和表达式的能力,可以让你更间接地表述许多常用的模式。

另外,分配(assignments)在Java中是表达式,在Kotlin中变成了语句(statements)。这可以帮助避免混淆类比(comparisons)和分配(assignments)。

EXPRESSION BODIES

前面的函数可以作进一步简化。因为它的语句体仅包含一个表达式,以及Kotlin的大部分控制结构都是表达式,有返回值。可以写为:

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

如果一个函数的语句体用花括号描述,我们可以说该函数有一个 块体(block body) 。如果它直接返回一个表达式,我们称其为 表达体(expression body)

带有表达体的函数不仅被用于琐碎的一行函数,也被用于单一但复杂的表达式,诸如ifwhentry

上面的max函数甚至可以省略返回类型:

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

为什么会有函数不带返回类型的声明?不仅Kotlin这样,作为一门静态类型语言,所有变量和表达式都有类型,所有函数都有返回类型。对于表达体函数,编译器可以分析出函数体内的表达式从而作为函数的返回类型,即使它没有显示拼写出来。这种分析的类型通常被称为 类型推导(type inference)

注意省略的函数类型仅允许表达体的函数。对于block body的函数需要显式使用return语句并指定返回类型。真实世界的函数通常会有多个return语句,拥有返回类型和return语句可以显式地帮助你捕获需要返回的内容。

Variables

在Java中,变量声明开始于类型。Kotlin则不是,使用关键字声明变量以省略变量类型的声明。

1
2
val question = "The Ultimate Question of Life, the Universe, and Everything"
val anser = 42

当然你也可以显式指定变量的类型:

1
val answer: Int = 42

和expression-body函数一样,如果你不指定类型,编译器分析并初始化表达式的类型来作为变量的类型。这里例子中,初始化为42,有Int类型,因此变量的类型为Int

如果使用一个浮点型常量,则变量的类型为Double

1
val yearsToCompute = 7.5e6

如果变量没有被初始化,你一定显式指定它的类型:

1
2
val answer: Int
answer = 42

因为你如果你不初始化,编译器无法推断出变量的类型信息。

MUTABLE AND IMMUTABLE VARIABLES

有两个关键字去定义变量:

  • val(from value): Immutable reference. 一个变量用val声明初始化后不可重新分配。对应于Java的final变量。
  • var(from variable): Mutable reference. 就是变量的值可以被修改。

默认地,你更应该使用val关键字来声明所有变量。仅在某些必要情况下使用var。使用不带副作用(side effects)的不可变引用、不可变对象、不可变函数,可以使得你的代码更接近函数风格。

val变量必须在它的定义语句块内初始化一次。但有时你也可以根据条件初始化不同的值,但前提是要确保编译器仅初始化一次:

1
2
3
4
5
6
7
8
val message: String
if (canPerformOperation()) {
message = "Success"
// ... perform the operation
}
else {
message = "Failed"
}

注意,尽管一个val自身的引用是不可变的不会被改变,但变量对象的指针则会被改变。例如:

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

尽管var关键字声明的对象的值会被改变,但它的类型是固定的。例如:

1
2
var anser = 42
answer = "no answer"

这会抛出错误,编译器在变量初始化的时候推断了变量的类型,不允许再分配不同的类型。

如果你需要存储一个类型不匹配的变量,必须手动转换或强制该值为合适的类型。

Easier string formatting: string templates

我们改下前面的例子,

1
2
3
4
fun main(args: Array<String>) {
val name = if (args.size > 0) args[0] else "Kotlin"
println("Hello, $name!")
}

这个例子引入了一个特性,字符串模板(string templates)。代码中,你声明了一个变量name并使用到字符字面量。和其它脚本语言一样,KotlinKotlin可以在字符串字面量中使用$字符来引用本地变量。这等价于Java的字符串拼接 (Hello, " + name + "!") 但相比更高效1。当然,改表达式是静态检查,如果引用的变量不存在,编译不会通过。

另外如果你需要在字符串引入$字符,则需要转义:println("\$x"),这样就仅仅是输出$x,不会把x推断为变量引用。

如果表达式太复杂,你不需要被强制简化变量名,可以使用花括号囊括表达式来描述:

1
2
3
4
5
fun main(args: Array<String>) {
if (args.size > 0) {
println("Hello, ${args[0]}!") // 使用`${}`表达式
}
}

或者双引号里面再囊括双引号,但前提是它必须是一个表达式:

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

Classes and properties

如果你对面向对象编程再熟悉不过,并熟知class的概念。那么Kotlin的本节内容对你也同样熟悉,但你会发现许多通用的任务可以使用更少的代码来完成。

让我们从JavaBean的Person类开始。

1
2
3
4
5
6
7
8
9
10
11
public class Person {
private final String name;

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

public String getName() {
return name;
}
}

在Java,构造器通常会用于初始化成员变量参数。在Kotlin中,这种逻辑可以不用这种样板代码(boilerplate code)来表述。Kotlin的写法更直接和简单。

1
class Person(val name: String)

这种仅包含数据但没有具体代码逻辑的类型通常被称为 值对象(value objects),许多语言提供了对其声明的简单语法。

注意到,修改器public在这里被省略了,因为在Kotlin中public默认就是可见的。

Properties

你肯定知道,一个类的设计是封装数据和代码,以及将对该数据的处理表述为一个单一实体类。在Java中,数据被存储在字段部分,通常是private的。如果你想通过外部访问改数据,你会提供 访问器方法(accessor methods);也就是getter和setter。setter可能会提供额外的逻辑来验证传入的值,发送通知等。

在Java中,accessor + private field的组合通常被作为 property 引用,需要一些Java框架甚至过度使用这个概念。在Kotlin,properties是first-class feature,它会完全替换 accessor + fields这种组合。你可以像定义变量一样来定义属性: 使用valvar关键字。属性使用val声明表示只读,使用var表示可变可修改。

1
2
3
4
class Person {
val name: String, // 只读属性:产生一个字段和一个繁琐的getter
var isMarried: Boolean // 可写属性:字段, getter, setter
}

基本上,当你声明一个属性,相应地声明了accessors(a getter for a read-only property, and both a getter and a setter for a writable one)。默认地,accessor的实现是琐碎的:创建一个字段来存储值,创建getter和setter来获取值以及更新值。但如果你需要,在Kotlin中可以自定义accessor覆盖原有的逻辑。

下面看看Java类如何使用:

1
2
3
4
5
>>> Person person = new Person("Bob", true);
>>> System.out.println(person.getName());
Bob
>>> System.out.println(person.isMarried());
true

然后是对比Kotlin的使用:

1
2
3
4
5
>>> val person = Person("Bob", true)    // 不带`new`关键字调用构造器
>>> println(person.name) // 直接访问属性。实际调用的是getter
Bob
>>> println(person.isMarried)
true

现在,相比调用getter,你可以直接地引用属性。代码逻辑不变,但更简洁。setter方法也一样,在Java,使用person.setMarried(false),对应Kotlin则person.isMarried = false

TIP 你也可以在Java中使用Kotlin的属性语法。Getters也可以在Kotlin中通过val属性访问,对应getter/setter通过var访问。

Custom accessors

本届向你展示如何自定义一个属性访问器。假设你声明了一个矩形,并可以判断它是否是正方形。是否是正方形的属性需要通过计算得出,因此:

1
2
3
4
5
6
class Rectangle(val height: Int, val width: Int) {
val isSquare: Boolean
get() { // 属性getter的声明
return height == width
}
}

属性isSquare不需要额外的字段来存储值。仅需要有一个自定义的getter提供即可。它的值在每次被访问时计算。

注意到你不需要每次都使用花括号;你可以写成get() = height == width。它的调用是等价的:

1
2
3
>>> val rectangle = Rectangle(41, 43)
>>> println(rectangle.isSquare)
false

在Java中直接访问Kotlin的isSquare即可。

Kotlin source code layout: directories and packages

Kotlin中对包的定义和引用和Java一样:在文件在最前面,使用package声明,使用import关键字引入,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
pacakge geometry.shapes

import java.util.Random

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

fun createRandomRectangle(): Rectangle {
val random = Random()
return Rectangle(random.nextInt(), random.nextInt())
}

另外Kotlin不区分引入包还是函数,所以你可以使用import关键引入任务需要使用的函数名。

1
2
3
4
5
6
7
package geometry.example

import geometry.shapes.createRandomRectangle

fun main(args: Array<String>) {
println(createRandomRectangle().isSquare) // 打印 "true"
}

你也可以使用.*这种wildcard来引入所有包内容。需要注意的是,星号不仅匹配的包下的所有类,同样也引入了包下的所有函数和属性。

在Java中,包的结构和目录是对应的,它要求类名和文件名必须有唯一的对应,如下,

Figure 2.2

但在Kotlin中,文件名不作要求,包名对应文件名,如下:

Figure 2.2

大多数情况下,还是建议遵循Java的包名习惯来组织代码目录。这对于Kotlin+Java的混合编程会比较清晰。

Representing and handling choices: enums and “when”

本小节将介绍when结构体。你可以认为它是Java的switch的一种替代,但它功能更强大,使用更频繁。顺便会介绍如何在Kotlin声明枚举,并讨论smart casts的概念。

Declaring enum classes

Kotlin的枚举和Java的枚举具有对应关系,声明用法如下:

1
2
3
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

这里的关键字部分enum class和Java的enum对应。在Kotlin中,enum被称为 软关键字(soft keyword)。在class前定义有特定含义。另外,class仍然是一个关键字。

在Java中,枚举不仅仅是值的列表:你可以在枚举类中声明属性和方法。在Kotlin中对应为:

1
2
3
4
5
6
7
8
9
10
11
12
enum class Color(val r: Int, val g: Int, val b: Int) {  // 声明枚举常量的属性
RED(255, 0 , 0), // 指定每个创建常量的属性值
ORANGE(255, 165, 0),
YELLOW(255, 255, 0),
GREEN(0, 255, 0),
BLUE(0, 0, 255),
INDIGO(75, 0, 130),
VIOLET(238, 130, 238); // 分号; 在这里是必须的

fun rgb() = (r * 156 + g) * 256 + b // 枚举类中定义的方法
}
>>> println(Color.BLUE.rgb())

枚举常量使用声明构造器和属性相同的语法。声明常量的同时你需要补充上相应的属性值。这里例子是唯一一个在Kotlin中使用分号(;)的地方:因为需要使用分号分离枚举常量和定义的方法。可以说Kotlin的枚举和Java没有区别~

Using “when” to deal with enum classes

when关键字对应于Java的switch用于处理多分支case。和ifwhen一样,when作为一个表达式可以用于返回值:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun getMnemonic(color: Color) =        // 直接返回一个"When" 表达式
when( color) { // 如果匹配,返回对应的字符串
Color.RED -> "Richard"
Color.ORANGE -> "Of"
Color.YELLOW -> "York"
Color.GREEN -> "Gave"
Color.BLUE -> "Battle"
Color.INDIGO -> "In"
Color.VIOLET -> "Vain"
}

>>> println(getMnemonic(Color.BLUE))
Battle

和Java不同,你不需要为每个分支带入break语句。如果匹配上,只有对应的分支被执行。你也可以在一个分支合并多个值:

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

>>> println(getWarmth(Color.ORANGE))
warm

例子使用了枚举常量的全名,你可以作进一步简化:

1
2
3
4
5
6
7
import Color.*                              // 导入Color类

fun getWarmth(color: Color) = when(color) {
RED, ORANGE, YELLOW -> "warm" // 直接使用常量名
GREEN -> "neutral"
BLUE, INDIGO, VIOLET -> "cold"
}

Using “when” with arbitrary objects

Kotlin中的when结构体比Java的switch强大多了。switch要求你使用常量(枚举常量,字符串、数字字面量)作为每个分支的条件,when除此还可以使用对象作为分支的条件。

1
2
3
4
5
6
7
8
9
10
fun mix(c1: Color, c2: Color) = 
when (setOf(c1, c2)) {
setOf(RED, YELLOW) -> ORANGE
setOf(YELLOW, BLUE) -> GREEN
setOf(BLUE, VIOLET) -> INDIGO
else -> throw Exception("Dirty color")
}

>>> pritnln(mix(BLUE, YELLOW))
GREEN

这里的setOf(c1, c2)实际上是一个等价checked:用于检测分支匹配,如果所有分支都不匹配,else分支则被调用。

Using “when” without an argument

上面的例子中,每次调用这个函数,它会创建Set实例用于检测分支。通常这没什么问题,但如果调用太过频繁会造成资源浪费。你可以改为如下不带参数的形式:

1
2
3
4
5
6
7
8
9
10
fun mixOptimized(c1: Color, c2: Color) = 
when { // No argument for "when"
(c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RD) -> ORANGE
(c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) -> GREEN
(c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) -> INDIGO
else -> throw Exception("Dirty color")
}

>>> println(mixOptimized(BLUE, YELLOW))
GREEN

如果没有提供任何参数,分支的条件变为任意的Boolean表达式。

吐槽: Kotlin没有模式解构,却用when这里奇奇怪怪的关键字+arbitrary objects来混分数。

Smart casts: combining type checks and casts

以算术运算式子为例,我们希望计算 (1 + 2) + 4 这样的算子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Expr
class Num(val value: Int): Expr
class Sum(val left: Expr, val right: Expr): Expr

fun eval(ex: Expr): Int {
if (e is Num) {
val n = e as 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

这里的eval函数传入了参数类型Expr。编译器会另起一个对象存储转换的对象类型,这样不用显式的使用as关键字就可以访问它的属性。

Refactoring: replacing “if” with “when”

前面说过,when关键字可以处理表达式,因此可以简化上面的代码:

1
2
3
4
5
6
fun eval(e: Expr): Int = 
when (e) {
is Num -> e.value // 检测参数的类型
is Sum -> eval(e.right) + eval(e.left) // 只能转换
else -> throw IllegalArgumentException("Unknown expression")
}

Blocks as branches of “if” and “when”

语句块也可以作为表达式部分返回值。譬如你希望在上面的例子作日志,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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("Unknown expression")
}

>>> println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
num: 1
num: 2
sum: 1 + 2
num: 4
sum: 3 + 4
7

Iterating over things: “while” and “for” loops

Kotlin中的迭代器原理是Java的复刻,不过语法上for的写法被改变了,Kotlin中引入了range的概念。

值域(range) 类似数学的区间、定义域,写法如下:

1
val oneToTen = 1..10

不过基于坐标都是从0开始,Kotlin的开区间(inclusive)和闭区间(closed),指的是第二个值是否被包含。

对值域的基本操作就是进行迭代操作,这样的值域称为 累进(progression)。

1
2
3
4
5
6
7
8
9
10
11
fun fizzBuzz(i: INt) = when {
i % 15 == 0 -> "FizzBuzz "
i % 3 == 0 -> "Fizz "
i % 5 == 0 -> "Buzz "
else -> "$i "
}
>>> for (i in 1..100) {
... print(fizzBuzzI0))
... }
}
1 2 Fizz 4 Buzz Fizz 7 ...

你可以指定递增步长,

1
2
3
4
>>> for (i in 100 downTo 1 step 2) {
print(fizzBuzz(i))
}
Buzz 98 Fizz 94 92 FizzBuzz 88 ...

step步长可以是负数,100 downTo 1表示后退到1,因此实际效果是从100开始-2…

如果想要设定终止条件,可以写为for (x in 0 until size),它等价于 for (x in 0..size-1)

Iterating over maps

1
2
3
4
5
6
7
8
9
10
val binaryReps = TreeMap<Char, String>()	// 使用TreeMap已是的key是排序的

for (c in 'A'..'F') {
val binary = Integer.toBinaryString(c.toInt()) // 将ASCII转换为二进制
binaryReps[c] = binary
}

for ((letter, binary) in binaryReps) {
println("$letter = $binary")
}

binaryReps[c] = binary 等价于Java的 binaryReps.put(c, binary)

你可以使用一些解包语法来获得迭代器的索引坐标,

1
2
3
4
val list = arrayListOf("10", "11", "1001")
for ((index, element) in list.withIndex()) {
println("$index: $element")
}

Using “in” to check collection and range membership

可以使用 in!in 操作符来检测一个值是否在Range内,

1
2
3
4
5
6
7
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

>>> println(isLetter('q'))
true
>>> println(isNotDigit('x'))
true

还可以和when表达式结合使用,

1
2
3
4
5
6
7
8
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

Range不强制要求它的类型是字符或数字,任何满足java.lang.Comparable接口的类都可以作为Range的值调用,考虑下面的代码:

1
2
>>> println("Kotlin" in "Java".."Scala")
true

为什么?它实际上对应于Java的"java" <= "Kotlin" && "Kotlin" <= "Scala",也就是调用了compareTo方法。

同理,对于setOf也是如此,

1
2
>>> println("Kotlin" in setOf("Java", "Scala"))  // 是否在集合内`contain`
false

Exceptions in Kotlin

Kotlin的异常处理和Java无异,

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

前面描述过了,Kotlin不使用new关键字

和Java的不同在于,throw结构体也是一个表达式,可以作为其它表达式的一部分使用。

1
2
3
4
5
6
val percentage = 
if (number in 0..100)
number
else
// "throw" is an expression.
throw IllegalArgumentException("A percentage value must be between 0 and 100: $number")

“try”, “catch”, and “finally”

你可以和Java一样在Kotlin中使用try-catch-finally三段式,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun readNUmber(reader: BufferedReader): Int? {
try {
val line = reader.readLine()
return Integer.parseInt(line)
}
catch (e: NumberFormatException) {
return null
}
finally {
reader.close()
}
}

>>> val reader = BufferedReader(StringReader("239"))
>>> println(readerNumber(reader))
239

和Java最大的不同在于这里的代码不出现throws在方法签名中:Kotlin没有区分检查和非检查异常。以及对于java7的try-with-resources也没有特殊的语法作处理,Kotlin中被实现为一个库函数。

“try” as an expression

另外一个不同在于,Kotlin中所有的关键字都是表达式,包括try关键字,因此可以被作为返回值

1
2
3
4
5
6
7
8
9
10
11
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch(e: NumberFormatException) {
return
}
println(number)
}

>>> val reader = BufferedReader(StringReader("no a number"))
>>> readNumber(reader) // 什么东西都没有打印

catch的时候被return了,所有打印语句没有执行。如果希望只需执行,可以在catch的块内添加一个值。

1
2
3
4
5
6
7
8
9
10
11
fun readNumber(reader: BufferedReader) {
val number = try {
Integer.parseInt(reader.readLine())
} catch(e: NumberFormatException) {
null // null 被作为表达式返回了
}
println(number)
}

>>> val reader = BufferedReader(StringReader("no a number"))
>>> readNumber(reader)

Summary

  • fun关键字用于声明一个函数。valvar对应只读和可变值声明。
  • 字符串模板使用${ }来避免样板代码。
  • 值对象(VO, value-object)在Kotlin中被得到简化,accessor(getter/setter)+field被重新定义为属性(property)。
  • if关键字被作为表达式。
  • when表达式对应于Java的switch语句。
  • 不需要显式地对类型进行转换,Kotlin提供了智能类型转换。
  • forwhile,和do-while和Java的类似,但Kotlin的for更简洁,功能更丰富。
  • 这种简洁的1..5语法用于创建Range。通常用于组合for循环和in!in操作符处理逻辑。
  • Kotlin的异常处理跟Java的如出一辙,但Kotlin不区分检查类异常和非检查类异常,以及不要求函数抛出异常,try-catch-finally关键字也是一个表达式,可以在适当的代码块内作为返回值。

  1. 为什么高效?这段编译的代码会创建一个StringBuilder将常量部分和变量值进行追加。