¶主要内容
- 一个基本Kotlin例子
- Kotlin语言的主要特质(traits)
- Android和服务端开发的可能性
- Kotlin和其它语言的不同
- 编写和运行Kotlin代码
¶Basic elements: functions and variables
本章节介绍构成每个Kotlin程序的基本要素:函数和变量。你将会看到Kotlin如何让你省略了许多类型声明,如何促使你使用不可变数据(immutable),而不是可变数据(mutable)。
¶Hello, world!
从"Hello World"开始:
1 | fun main(args: Array<String>) { |
从这个简单的代码片段可以一瞥语言的部分特性(feature):
fun
关键字用于声明函数。- 参数类型写在参数名之后。变量的声明也如此。
- 函数可以声明在一个文件的最顶层;你不需要将其放置在一个类内声明。
- Kotlin中的数组就是类。和Java不同,Kotlin没有对数组类型的特定语法。
- 使用
println
而不是System.out.println
。Kotlin标准库使用简洁的语法,提供了许多关于Java库函数的包装(wrappers),println
就是其中之一。 - 和其它现代语言一样,你可以在行末省略分号(; )。
¶Functions
如果希望函数包含返回类型,如何在签名中定义?
1 | fun max(a: Int, b: Int): Int { |
函数的声明由fun
关键字开始,随后是函数名,例如这里的max
。之后是括号内的参数列表。返回类型跟随在参数列表之后,由冒号分隔。
图Figure 2.1 展示了一个函数的基本结构。注意在Kotlin,if
是一个带有结果的表达式。类似于Java的三元操作:(a > b) ? a : b
。
¶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)。
带有表达体的函数不仅被用于琐碎的一行函数,也被用于单一但复杂的表达式,诸如if
、when
或try
。
上面的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 | val question = "The Ultimate Question of Life, the Universe, and Everything" |
当然你也可以显式指定变量的类型:
1 | val answer: Int = 42 |
和expression-body函数一样,如果你不指定类型,编译器分析并初始化表达式的类型来作为变量的类型。这里例子中,初始化为42,有Int
类型,因此变量的类型为Int
。
如果使用一个浮点型常量,则变量的类型为Double
:
1 | val yearsToCompute = 7.5e6 |
如果变量没有被初始化,你一定显式指定它的类型:
1 | val answer: Int |
因为你如果你不初始化,编译器无法推断出变量的类型信息。
MUTABLE AND IMMUTABLE VARIABLES
有两个关键字去定义变量:
val
(from value): Immutable reference. 一个变量用val
声明初始化后不可重新分配。对应于Java的final变量。var
(from variable): Mutable reference. 就是变量的值可以被修改。
默认地,你更应该使用val
关键字来声明所有变量。仅在某些必要情况下使用var
。使用不带副作用(side effects)的不可变引用、不可变对象、不可变函数,可以使得你的代码更接近函数风格。
val
变量必须在它的定义语句块内初始化一次。但有时你也可以根据条件初始化不同的值,但前提是要确保编译器仅初始化一次:
1 | val message: String |
注意,尽管一个val
自身的引用是不可变的不会被改变,但变量对象的指针则会被改变。例如:
1 | val languages = arrayListOf("Java") |
尽管var
关键字声明的对象的值会被改变,但它的类型是固定的。例如:
1 | var anser = 42 |
这会抛出错误,编译器在变量初始化的时候推断了变量的类型,不允许再分配不同的类型。
如果你需要存储一个类型不匹配的变量,必须手动转换或强制该值为合适的类型。
¶Easier string formatting: string templates
我们改下前面的例子,
1 | fun main(args: Array<String>) { |
这个例子引入了一个特性,字符串模板(string templates)。代码中,你声明了一个变量name
并使用到字符字面量。和其它脚本语言一样,KotlinKotlin可以在字符串字面量中使用$
字符来引用本地变量。这等价于Java的字符串拼接 (Hello, " + name + "!")
但相比更高效1。当然,改表达式是静态检查,如果引用的变量不存在,编译不会通过。
另外如果你需要在字符串引入$
字符,则需要转义:println("\$x")
,这样就仅仅是输出$x
,不会把x
推断为变量引用。
如果表达式太复杂,你不需要被强制简化变量名,可以使用花括号囊括表达式来描述:
1 | fun main(args: Array<String>) { |
或者双引号里面再囊括双引号,但前提是它必须是一个表达式:
1 | fun main(args: Array<String>) { |
¶Classes and properties
如果你对面向对象编程再熟悉不过,并熟知class
的概念。那么Kotlin的本节内容对你也同样熟悉,但你会发现许多通用的任务可以使用更少的代码来完成。
让我们从JavaBean的Person
类开始。
1 | public class Person { |
在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这种组合。你可以像定义变量一样来定义属性: 使用val
和 var
关键字。属性使用val
声明表示只读,使用var
表示可变可修改。
1 | class Person { |
基本上,当你声明一个属性,相应地声明了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 | >>> Person person = new Person("Bob", true); |
然后是对比Kotlin的使用:
1 | >>> val person = Person("Bob", true) // 不带`new`关键字调用构造器 |
现在,相比调用getter,你可以直接地引用属性。代码逻辑不变,但更简洁。setter方法也一样,在Java,使用person.setMarried(false)
,对应Kotlin则person.isMarried = false
。
TIP 你也可以在Java中使用Kotlin的属性语法。Getters也可以在Kotlin中通过val
属性访问,对应getter/setter通过var
访问。
¶Custom accessors
本届向你展示如何自定义一个属性访问器。假设你声明了一个矩形,并可以判断它是否是正方形。是否是正方形的属性需要通过计算得出,因此:
1 | class Rectangle(val height: Int, val width: Int) { |
属性isSquare
不需要额外的字段来存储值。仅需要有一个自定义的getter提供即可。它的值在每次被访问时计算。
注意到你不需要每次都使用花括号;你可以写成get() = height == width
。它的调用是等价的:
1 | >>> val rectangle = Rectangle(41, 43) |
在Java中直接访问Kotlin的isSquare
即可。
¶Kotlin source code layout: directories and packages
Kotlin中对包的定义和引用和Java一样:在文件在最前面,使用package
声明,使用import
关键字引入,写法如下:
1 | pacakge geometry.shapes |
另外Kotlin不区分引入包还是函数,所以你可以使用import
关键引入任务需要使用的函数名。
1 | package geometry.example |
你也可以使用.*
这种wildcard来引入所有包内容。需要注意的是,星号不仅匹配的包下的所有类,同样也引入了包下的所有函数和属性。
在Java中,包的结构和目录是对应的,它要求类名和文件名必须有唯一的对应,如下,
但在Kotlin中,文件名不作要求,包名对应文件名,如下:
大多数情况下,还是建议遵循Java的包名习惯来组织代码目录。这对于Kotlin+Java的混合编程会比较清晰。
¶Representing and handling choices: enums and “when”
本小节将介绍when
结构体。你可以认为它是Java的switch
的一种替代,但它功能更强大,使用更频繁。顺便会介绍如何在Kotlin声明枚举,并讨论smart casts
的概念。
¶Declaring enum classes
Kotlin的枚举和Java的枚举具有对应关系,声明用法如下:
1 | enum class Color { |
这里的关键字部分enum class
和Java的enum
对应。在Kotlin中,enum
被称为 软关键字(soft keyword)。在class
前定义有特定含义。另外,class
仍然是一个关键字。
在Java中,枚举不仅仅是值的列表:你可以在枚举类中声明属性和方法。在Kotlin中对应为:
1 | enum class Color(val r: Int, val g: Int, val b: Int) { // 声明枚举常量的属性 |
枚举常量使用声明构造器和属性相同的语法。声明常量的同时你需要补充上相应的属性值。这里例子是唯一一个在Kotlin中使用分号(;)的地方:因为需要使用分号分离枚举常量和定义的方法。可以说Kotlin的枚举和Java没有区别~
¶Using “when” to deal with enum classes
when
关键字对应于Java的switch
用于处理多分支case。和if
,when
一样,when
作为一个表达式可以用于返回值:
1 | fun getMnemonic(color: Color) = // 直接返回一个"When" 表达式 |
和Java不同,你不需要为每个分支带入break
语句。如果匹配上,只有对应的分支被执行。你也可以在一个分支合并多个值:
1 | fun getWarmth(color: Color) = when(color) { |
例子使用了枚举常量的全名,你可以作进一步简化:
1 | import Color.* // 导入Color类 |
¶Using “when” with arbitrary objects
Kotlin中的when
结构体比Java的switch
强大多了。switch
要求你使用常量(枚举常量,字符串、数字字面量)作为每个分支的条件,when
除此还可以使用对象作为分支的条件。
1 | fun mix(c1: Color, c2: Color) = |
这里的setOf(c1, c2)
实际上是一个等价checked:用于检测分支匹配,如果所有分支都不匹配,else
分支则被调用。
¶Using “when” without an argument
上面的例子中,每次调用这个函数,它会创建Set
实例用于检测分支。通常这没什么问题,但如果调用太过频繁会造成资源浪费。你可以改为如下不带参数的形式:
1 | fun mixOptimized(c1: Color, c2: Color) = |
如果没有提供任何参数,分支的条件变为任意的Boolean表达式。
吐槽: Kotlin没有模式解构,却用when
这里奇奇怪怪的关键字+arbitrary objects来混分数。
¶Smart casts: combining type checks and casts
以算术运算式子为例,我们希望计算 (1 + 2) + 4 这样的算子,
1 | interface Expr |
这里的eval
函数传入了参数类型Expr
。编译器会另起一个对象存储转换的对象类型,这样不用显式的使用as
关键字就可以访问它的属性。
¶Refactoring: replacing “if” with “when”
前面说过,when
关键字可以处理表达式,因此可以简化上面的代码:
1 | fun eval(e: Expr): Int = |
¶Blocks as branches of “if” and “when”
语句块也可以作为表达式部分返回值。譬如你希望在上面的例子作日志,
1 | fun evalWithLogging(e: Expr): Int = |
¶Iterating over things: “while” and “for” loops
Kotlin中的迭代器原理是Java的复刻,不过语法上for
的写法被改变了,Kotlin中引入了range
的概念。
值域(range) 类似数学的区间、定义域,写法如下:
1 | val oneToTen = 1..10 |
不过基于坐标都是从0开始,Kotlin的开区间(inclusive)和闭区间(closed),指的是第二个值是否被包含。
对值域的基本操作就是进行迭代操作,这样的值域称为 累进(progression)。
1 | fun fizzBuzz(i: INt) = when { |
你可以指定递增步长,
1 | >>> for (i in 100 downTo 1 step 2) { |
step
步长可以是负数,100 downTo 1
表示后退到1,因此实际效果是从100开始-2…
如果想要设定终止条件,可以写为for (x in 0 until size)
,它等价于 for (x in 0..size-1)
。
¶Iterating over maps
1 | val binaryReps = TreeMap<Char, String>() // 使用TreeMap已是的key是排序的 |
binaryReps[c] = binary
等价于Java的 binaryReps.put(c, binary)
。
你可以使用一些解包语法来获得迭代器的索引坐标,
1 | val list = arrayListOf("10", "11", "1001") |
¶Using “in” to check collection and range membership
可以使用 in
和 !in
操作符来检测一个值是否在Range内,
1 | fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z' |
还可以和when
表达式结合使用,
1 | fun recognize(c: Char) = when (c) { |
Range不强制要求它的类型是字符或数字,任何满足java.lang.Comparable
接口的类都可以作为Range的值调用,考虑下面的代码:
1 | >>> println("Kotlin" in "Java".."Scala") |
为什么?它实际上对应于Java的"java" <= "Kotlin" && "Kotlin" <= "Scala"
,也就是调用了compareTo
方法。
同理,对于setOf
也是如此,
1 | >>> println("Kotlin" in setOf("Java", "Scala")) // 是否在集合内`contain` |
¶Exceptions in Kotlin
Kotlin的异常处理和Java无异,
1 | if (percentage !in 0..100) { |
前面描述过了,Kotlin不使用new
关键字
和Java的不同在于,throw
结构体也是一个表达式,可以作为其它表达式的一部分使用。
1 | val percentage = |
¶“try”, “catch”, and “finally”
你可以和Java一样在Kotlin中使用try-catch-finally
三段式,
1 | fun readNUmber(reader: BufferedReader): Int? { |
和Java最大的不同在于这里的代码不出现throws
在方法签名中:Kotlin没有区分检查和非检查异常。以及对于java7的try-with-resources
也没有特殊的语法作处理,Kotlin中被实现为一个库函数。
¶“try” as an expression
另外一个不同在于,Kotlin中所有的关键字都是表达式,包括try
关键字,因此可以被作为返回值
1 | fun readNumber(reader: BufferedReader) { |
在catch
的时候被return
了,所有打印语句没有执行。如果希望只需执行,可以在catch
的块内添加一个值。
1 | fun readNumber(reader: BufferedReader) { |
¶Summary
fun
关键字用于声明一个函数。val
和var
对应只读和可变值声明。- 字符串模板使用
${ }
来避免样板代码。 - 值对象(VO, value-object)在Kotlin中被得到简化,accessor(getter/setter)+field被重新定义为属性(property)。
if
关键字被作为表达式。when
表达式对应于Java的switch
语句。- 不需要显式地对类型进行转换,Kotlin提供了智能类型转换。
for
,while
,和do-while
和Java的类似,但Kotlin的for
更简洁,功能更丰富。- 这种简洁的
1..5
语法用于创建Range。通常用于组合for
循环和in
,!in
操作符处理逻辑。 - Kotlin的异常处理跟Java的如出一辙,但Kotlin不区分检查类异常和非检查类异常,以及不要求函数抛出异常,
try-catch-finally
关键字也是一个表达式,可以在适当的代码块内作为返回值。
- 为什么高效?这段编译的代码会创建一个
StringBuilder
将常量部分和变量值进行追加。 ↩