在 Java 平台中, 数值的物理存储使用 JVM 的基本类型来实现, 但当我们需要表达一个可为 null 的数值引用时(比如. Int?
), 或者涉及到泛型时, 我们就不能使用基本类型了. 这种情况下数值会被装箱(box)为数值对象.
注意, 数值对象的装箱(box)并不保持对象的同一性(identity):
val a: Int = 10000
print(a === a) // 打印结果为 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA === anotherBoxedA) // !!!打印结果为 'false'!!!
但是, 装箱(box)会保持对象内容相等(equality):
val a: Int = 10000
print(a == a) // 打印结果为 'true'
val boxedA: Int? = a
val anotherBoxedA: Int? = a
print(boxedA == anotherBoxedA) // 打印结果为 'true'
Kotlin 中的数组通过 Array
类表达, 这个类拥有 get
和 set
函数(这些函数通过运算符重载转换为 []
运算符), 此外还有 size
属性, 以及其他一些有用的成员函数:
class Array<T> private constructor() {
val size: Int
fun get(index: Int): T
fun set(index: Int, value: T): Unit
fun iterator(): Iterator<T>
// ...
}
要创建一个数组, 我们可以使用库函数 arrayOf()
, 并向这个函数传递一些参数来指定数组元素的值, 所以 arrayOf(1, 2, 3)
将创建一个数组, 其中的元素为 [1, 2, 3]. 或者, 也可以使用库函数 arrayOfNulls()
来创建一个指定长度的数组, 其中的元素全部为 null 值.
另一种方案是使用一个工厂函数, 第一个参数为数组大小, 第二个参数是另一个函数, 这个函数接受数组元素下标作为自己的输入参数, 然后返回这个下标对应的数组元素的初始值:
// 创建一个 Array, 其中的元素为 ["0", "1", "4", "9", "16"]
val asc = Array(5, { i -> (i * i).toString() })
我们在前面提到过, []
运算符可以用来调用数组的成员函数 get()
和 set()
.
注意: 与 Java 不同, Kotlin 中数组的类型是不可变的. 所以 Kotlin 不允许将一个 Array
赋值给一个 Array
, 否则可能会导致运行时错误(但你可以使用 Array
, 参见 类型投射).
Kotlin 中也有专门的类来表达基本数据类型的数组: ByteArray
, ShortArray
, IntArray
等等, 这些数组可以避免数值对象装箱带来的性能损耗. 这些类与 Array
类之间不存在继承关系, 但它们的方法和属性是一致的. 各个基本数据类型的数组类都有对应的工厂函数:
val x: IntArray = intArrayOf(1, 2, 3)
x[0] = x[1] + x[2]
字符串由 String
类型表示. 字符串的内容是不可变的. 字符串中的元素是字符, 可以通过下标操作符来访问: s[i]
. 可以使用 for 循环来遍历字符串:
val s = "Hello, world!\n"
"""
), 其内容不转义, 可以包含换行符和任意字符:
val text = """
for (c in "foo")
print(c)
"""
字符串内可以包含模板表达式, 也就是说, 可以包含一小段代码, 这段代码会被执行, 其计算结果将被拼接为字符串内容的一部分. 模板表达式以 $ 符号开始, $ 符号之后可以是一个简单的变量名:
val i = 10
val s = "i = $i" // 计算结果为 "i = 10"
$ 符号之后也可以是任意的表达式, 由大括号括起:
val s = "abc"
val str = "$s.length is ${s.length}" // 计算结果为 "abc.length is 3"
原生字符串(raw string)和转义字符串(escaped string)内都支持模板. 由于原生字符串无法使用反斜线转
源代码文件的开始部分可以是包声明:
package foo.bar
fun baz() {}
class Goo {}
// ...
源代码内的所有内容(比如类, 函数)全部都包含在所声明的包之内. 因此, 上面的示例代码中, baz()
函数的完整名称将是 foo.bar.baz
, Goo
类的完整名称将是 foo.bar.Goo
.
如果没有指定包, 那么源代码文件中的内容将属于 “default” 包, 这个包没有名称.
除默认导入(Import)的内容之外, 各源代码可以包含自己独自的 import 指令. import 指令的语法请参见 语法.
我们可以导入一个单独的名称, 比如
import foo.Bar // 导入后 Bar 就可以直接访问, 不必指定完整的限定符
也可以导入某个范围(包, 类, 对象, 等等)之内所有可访问的内容:
import foo.* // 导入后 'foo' 内的一切都可以访问了
如果发生了名称冲突, 我们可以使用 as 关键字, 给重名实体指定新的名称(新名称仅在当前范围内有效):
import foo.Bar // 导入后 Bar 可以访问了
import bar.Bar as bBar // 可以使用新名称 bBar 来访问 'bar.Bar'
import
关键字不仅可以用来导入类; 还可以用来导入其他声明:
与 Java 不同, Kotlin 没有单独的 “import static” 语法; 所有这些声明都使用通常的 import
关键字来表达.
在 Kotlin 中, if 是一个表达式, 也就是说, 它有返回值. 因此, Kotlin 中没有三元运算符(条件 ? then 分支返回值 : else 分支返回值), 因为简单的 if 表达式完全可以实现同样的任务.
// if 的传统用法
var max = a
if (a < b) max = b
// 使用 else 分支的方式
var max: Int
if (a > b) {
max = a
} else {
max = b
}
// if 作为表达式使用
val max = if (a > b) a else b
if 的分支可以是多条语句组成的代码段, 代码段内最后一个表达式的值将成为整个代码段的返回值:
val max = if (a > b) {
print("Choose a")
a
} else {
print("Choose b")
b
}
如果你将 if 作为表达式来使用(比如, 将它的值作为函数的返回值, 或将它的值赋值给一个变量), 而不是用作普通的流程控制语句, 这种情况下 if 表达式必须有 else
分支.
when 替代了各种 C 风格语言中的 switch 操作符. 最简单的形式如下例:
when (x) {
1 -> print("x == 1")
2 -> print("x == 2")
else -> { // 注意, 这里是代码段
print("x is neither 1 nor 2")
}
}
when 语句会将它的参数与各个分支逐个匹配, 直到找到某个分支的条件成立. when 可以用作表达式, 也可以用作流程控制语句. 如果用作表达式, 满足条件的分支的返回值将成为整个表达式的值. 如果用作流程控制语句, 各个分支的返回值将被忽略. (与 if 类似, 各个分支可以是多条语句组成的代码段, 代码段内最后一个表达式的值将成为整个代码段的值.)
如果其他所有分支的条件都不成立, 则会执行 else 分支. 如果 when 被用作表达式, 则必须有 else 分支, 除非编译器能够证明其他分支的条件已经覆盖了所有可能的情况.
如果对多种条件需要进行相同的处理, 那么可以对一个分支指定多个条件, 用逗号分隔:
when (x) {
0, 1 -> print("x == 0 or x == 1")
else -> print("otherwise")
}
在分支条件中, 我们可以使用任意的表达式(而不仅仅是常数值)
when (x) {
parseInt(s) -> print("s encodes x")
else -> print("s does not encode x")
}
我们还可以使用 in 或 !in 来检查一个值是否属于一个 范围, 或者检查是否属于一个集合:
when (x) {
in 1..10 -> print("x is in the range")
in validNumbers -> print("x is valid")
!in 10..20 -> print("x is outside the range")
else -> print("none of the above")
}
还可以使用 is 或 !is 来检查一个值是不是某个类型. 注意, 由于 Kotlin 的 智能类型转换 功能, 进行过类型判断之后, 你就可以直接访问这个类型的方法和属性, 而不必再进行显式的类型检查.
val hasPrefix = when(x) {
is String -> x.startsWith("prefix")
else -> false
}
when 也可以用来替代 if-else if 串. 如果没有指定参数, 那么所有的分支条件都应该是单纯的布尔表达式, 当条件的布尔表达式值为 true 时, 就会执行对应的分支:
when {
x.isOdd() -> print("x is odd")
x.isEven() -> print("x is even")
else -> print("x is funny")
}
任何值, 只要能够产生一个迭代器(iterator), 就可以使用 for 循环进行遍历. 语法如下:
for (item in collection) print(item)
循环体可以是多条语句组成的代码段.
for (item: Int in ints) {
// ...
}
前面提到过, 凡是能够产生迭代器(iterator)的值, 都可以使用 for 进行遍历, 也就是说, 遍历对象需要满足以下条件:
iterator()
, 它的返回类型应该:
next()
, 并且hasNext()
, 它的返回类型为 Boolean
类型. 上述三个函数都需要标记为 operator
.
for
循环遍历数组时, 会被编译为基于数组下标的循环, 不会产生迭代器(iterator)对象.
如果你希望使用下标变量来遍历数组或 List, 可以这样做:
for (i in array.indices) {
print(array[i])
}
注意, 上例中的 “在数值范围内的遍历” 会在编译期间进行优化, 运行时不会产生额外的对象实例.
或者, 你也可以使用 withIndex
库函数:
for ((index, value) in array.withIndex()) {
println("the element at $index is $value")
}
while 和 do..while 的功能与其他语言一样:
while (x > 0) {
x--
}
do {
val y = retrieveData()
} while (y != null) // y is visible here!
Kotlin 中存在 3 种程序流程跳出操作符
Kotlin 中的任何表达式都可以用 label 标签来标记. 标签的形式与标识符相同, 后面附加一个 @
符号, 比如: abc@
, fooBar@
都是合法的标签(参见 语法). 要给一个表达式标记标签, 我们只需要将标签放在它之前:
loop@ for (i in 1..100) {
// ...
}
然后, 我们就可以使用标签来限定 break 或 continue 的跳转对象:
loop@ for (i in 1..100) {
for (j in 1..100) {
if (...) break@loop
}
}
通过标签限定后, break 语句, 将会跳转到这个标签标记的循环语句之后. continue 语句则会跳转到循环语句的下一次循环.
在 Kotlin 中, 通过使用字面值函数(function literal), 局部函数(local function), 以及对象表达式(object expression), 允许实现函数的嵌套. 通过标签限定的 return 语句, 可以从一个外层函数中返回. 最重要的使用场景是从 Lambda 表达式中返回. 回忆一下我们曾经写过以下代码:
fun foo() {
ints.forEach {
if (it == 0) return
print(it)
}
}
这里的 return 会从最内层的函数中返回, 也就是. 从 foo
函数返回. (注意, 这种非局部的返回( non-local return), 仅对传递给 内联函数(inline function) 的 Lambda 表达式有效.) 如果需要从 Lambda 表达式返回, 我们必须对它标记一个标签, 然后使用这个标签来指明 return 的目标:
fun foo() {
ints.forEach lit@ {
if (it == 0) return@lit
print(it)
}
}
这样, return 语句就只从 Lambda 表达式中返回. 通常, 使用隐含标签会更方便一些, 隐含标签的名称与 Lambda 表达式被传递去的函数名称相同.
fun foo() {
ints.forEach {
if (it == 0) return@forEach
print(it)
}
}
或者, 我们也可以使用 匿名函数 来替代 Lambda 表达式. 匿名函数内的 return 语句会从匿名函数内返回.
fun foo() {
ints.forEach(fun(value: Int) {
if (value == 0) return
print(value)
})
}
当 return 语句指定了返回值时, 源代码解析器会将这样的语句优先识别为使用标签限定的 return 语句, 也就是说:
return@a 1
含义是 “返回到标签 @a
处, 返回值为 1
”, 而不是 “返回一个带标签的表达式 (@a 1)
”.