9.3 Kotlin语言基础
学习Kotlin时,建议你随时参阅Kotlin参考文档。要找到这个文档,可单击Kotlin官网主页中的链接LEARN,也可直接访问http://kotlinlang.org/docs/reference/。
本节介绍如下主题:
- 定义局部变量;
- 定义函数;
- Kotlin类型;
- 循环。
9.3.1 定义局部变量
要定义局部变量,可使用var或val:
var aMutableNumber = 24val anImmutableNumber = 42
区别在于使用var定义的变量是可以修改的,而使用val定义的变量是不可修改的。定义变量时还可指定其类型:
var aMutableString: String = "A type can optionally be specified..."val anImmutableString: String = "...no matter whether you are usingvar or val"
支持的类型将在9.3.3节讨论。要将null赋给变量,务必采取必要的预防措施。Kotlin的类型系统比较独特,针对可为null的变量制订了一些使用规则,这也将在9.3.3节讨论。
变量可在函数或类中定义,也可在代码开头定义(采用过程性编程时),这些情形都将在相关的章节中讨论。
你可按下面的规则使用字面量:
- 不带后缀的整数为
Int; - 要使用
Long字面量,必须指定后缀L; - 可使用十六进制表示
Int和Long,为此可加上前缀0x; - 可使用二进制表示整数,为此可加上前缀0b;
- 不带后缀的浮点数为
Double; - 带后缀f或F的浮点数为
Float; Float和Double值都可使用科学记数法表示。
下面是一些示例:
val thisIsAnInt = 42val thisIsALong = 1000Lval hexInt = 0xFFval binaryLong = 0b10101100Lval thisIsADouble = 149.16val thisIsAFloat = 501.19e2f
Kotlin不像Java那样使用关键字new来实例化对象,而像使用函数那样使用类名:
class A (i: Int) {}val a = A(25)
上述代码定义了类A,它包含接受一个Int输入参数的主构造函数。接下来,实例化了这个类:在类名后加上构造函数参数的值。
9.3.2 定义函数
要定义函数,可使用关键字fun:
fun functionName() {}
当然,也可给函数指定参数,但指定参数时必须指定其类型:
fun functionNameWithParameters(i: Int, j: Int) {println(i * j)}
如果没有指定返回类型(就像前面两个示例那样),函数的返回类型默认为Unit。Unit相当于Java中的void,不同之处在于可将Unit返回值赋给变量:
fun noReturnValue(x: Int, y: Int): Unit {val f = noReturnValue(1, 2)println(f)
如果在REPL中输入并执行上述代码片段,将打印kotlin.Unit。下面是一个返回整数(Kotlin类型Int)的函数:
fun returnsAnInt(x: Int, y: Int): Int {return x * y}val f = returnsAnInt(10, 10)println(f)
如果函数只包含一行代码,定义它时可不使用大括号({ }),而使用运算符=。对于只包含一行代码的函数,可不显式地指定返回类型,因为返回类型可根据代码推断出来:
fun alsoReturnsAnInt(x: Int, y: Int) = x * y
9.3.3 Kotlin类型
Kotlin的一个独特之处是其类型系统,它在处理null引用方面尤其独特。本节讨论如下主题:
- Kotlin基本类型;
- 字符串;
- 安全地处理
null; - 转换;
集合和泛型。
Kotlin基本类型
Kotlin不直接使用JVM数据类型,而是将它们包装成自己的类型,这样做的原因之一是Kotlin支持多个编译目标(当前为JVM、Android和JavaScript)。通过使用自己的类型,可确保不同平台上的功能一致。但Kotlin依然与使用JVM数据类型(如基本类型以及java.lang.Integer和java.lang.String等常见类)的JVM代码完全兼容,这是因为调用Java代码时,Kotlin编译器会自动在JVM类型和Kotlin内部类型之间进行转换。
下表列出了Kotlin中最重要的基本类型及其全限定类型名,以及对应的JVM类型。
Kotlin类型名Kotlin全限定类型名对应的JVM类型 Bytekotlin.Byte基本类型byte Byte?kotlin.Byte?java.lang.Byte Doublekotlin.Double基本类型double Double?kotlin.Double?java.lang.Double Floatkotlin.Float基本类型float Float?kotlin.Float?java.lang.Float Intkotlin.Int基本类型int Int?kotlin.Int?java.lang.Integer Longkotlin.Long基本类型long Long?kotlin.Long?java.lang.Long Shortkotlin.Short基本类型short Short?kotlin.Short?java.lang.Short Anykotlin.Anyjava.lang.Object Stringkotlin.Stringjava.lang.String
稍后将详细说明类型名后面的问号的含义。就目前而言,你只需知道仅当变量是带问号的类型时,才能为null引用就够了。其类型不带问号的变量不能为null。
有趣的是,Kotlin类型很像普通类(例如,每种类型好像都有方法),但在内部,前述很多类型都在可能的情况下直接使用JVM基本类型。这方面的工作完全是由编译器在幕后处理的,这极大地改善了性能,因为无需不分青红皂白地将基本类型值自动装箱。
- 字符串
类型kotlin.String功能强大却易于使用,你可以像使用Java类java.lang.String那样使用它:
val s: String = "Hello!"
Kotlin也支持原始字符串,这种字符串可横跨多行:
val s: String = """raw string"""
不同于常规字符串,在原始字符串中,可使用斜杠对字符进行转义(例如,\n表示换行符,\t表示制表符)。
还支持字符串模板:
var favoriteBar = "FooBar"println("Your favorite bar's name $favoriteBar consists of${favoriteBar.length} characters")
这将打印Your favorite bar's name FooBar consists of 6 characters。
请注意,原始字符串不支持字符串模板。
- 安全地处理
null
前面多次说过,Kotlin可避免将null赋给引用变量导致的错误。对于普通类型变量,如果你将null赋给它,Kotlin将拒绝编译。
例如,下面的代码无法通过编译:
var currentTime = java.util.Date()// 下面这行代码无法通过编译currentTime = null
前面说过,实例化类时,Kotlin不要求你使用关键字new(Kotlin根本就不支持这个关键字)。如果你运行上述代码,将出现如下错误:
error: null can not be a value of a non-null type Date
要让这些代码通过编译,必须在变量的类型名后面加上问号:
var currentTime: java.util.Date? = java.util.Date()// 现在这行代码能够通过编译currentTime = null
要调用currentTime的方法或访问其其他成员,必须让编译器知道,这个引用可能为null,也可能不为null。例如,下面的代码无法通过编译:
var currentTime: java.util.Date? = java.util.Date()// 下面这行代码无法通过编译var seconds = currentTime.getTime()
这将导致如下错误:
error: only safe (?.) or non-null asserted (!!.) calls are allowed on anullable receiver of type Date?
在上述代码片段中,虽然currentTime不是null引用,但你必须告诉Kotlin编译器,你知道currentTime有可能为null。解决方案有多种:
- 添加条件检查;
- 使用安全调用运算符
?.; - 使用Elvis运算符
?:; 使用运算符
!!。方法1:添加条件检查
通过添加一条if语句,可告诉编译器,你知道引用变量有可能为null:
fun test() {var currentTime: java.util.Date? = java.util.Date()println("Line below will now compile fine")var seconds = if (currentTime != null) currentTime.getTime() else 0println(seconds)}test()
编译器得知你意识到这个实例变量可能为null后,就会心满意足地编译代码。以这种方式使用时,if条件返回currentTime.getTime()或0。在前述示例中,调用test()时将打印currentTime.getTime()的输出,因为currentTime不是null引用。
请注意,仅当编译器知道其他线程无法访问这个变量时,这种做法才管用。由于currentTime是在函数中定义的,其他线程确实无法访问它。如果currentTime是一个类的公有字段,编译器依然会拒绝编译这些代码,因为在执行if(currentTime != null)检查之后到执行currentTime.getTime()调用之前,其他线程可能修改currentTime。在这种情况下,必须采用其他方法,否则编译器将拒绝编译,并显示一条错误消息。
- 方法2:使用安全调用运算符
?.
Kotlint提供了安全调用运算符?.(问号后面跟一个句点),它在引用为null时返回null,否则就调用指定的方法或访问指定的成员:
var currentTime: java.util.Date? = nullvar seconds = currentTime?.getTime()println(seconds)
调用test()时,将打印null,因为执行currentTime?.getTime()时,Kotlin发现currentTime是一个null引用。如果currentTime指向一个java.util.Date实例,Kotlin将打印方法getTime()的输出。
运算符?.的一个优点是可以串接,下面是一个虚构的示例:
member1?.member2()?.member3()
如果属性member1为null引用或方法member2返回null,整个表达式的结果都将为null。如果member1和member2的输出都不是null引用,将返回方法member3的输出。
- 方法3:使用Elvis运算符
?:
对于第一个示例中的if语句,可使用Elvis运算符?:进行改写,得到的代码更简洁:
var currentTime: java.util.Date? = nullvar seconds = currentTime?.getTime() ?: -1println(seconds)
上述代码将返回-1。这是因为currentTime?.getTime()返回null(安全调用运算符?.返回null,因为currentTime为null引用),因此返回Elvis运算符?:后面的字面量-1。如果currentTime指向一个java.util.Date实例,上述代码将打印getTime()的输出。
- 方法4:使用
!!运算符
Kotlin官方文档指出,这个运算符就是为喜欢异常NullPointerException的人设计的。
通过在变量名后面加上运算符!!,可让Kotlin编译器完全忽略null安全系统。如果这个实例变量为null引用,而代码试图调用其方法或访问其成员,Kotlin将引发NullPointerException异常,就像Java在这种情况下的做法一样:
fun test() {var currentTime: java.util.Date? = nullprintln("Next line compiles, but throws exception when running")var seconds = currentTime!!.getTime()println(seconds)}test()
- 转换
在Java中,编译器在确定不会降低精度时,将自动进行转换。例如,下面的做法在Java中是合法的:
// Java代码int a = 1000;long b = a;
由于将int值存储到long变量中时,不会降低精度,因此Java自动将其转换为long。Kotlin不会自动转换变量,而要求程序员手动进行转换:
val a: Int = 1000val b: Long = a.toLong()
为支持转换,Kotlin的每种数值类型(Int、Long等)都包含如下方法:
toByte();toChar();toDouble();toFloat();toInt();toLong();toShort()。- 集合和泛型
与Scala和Clojure一样,Kotlin也提供了自己的集合类实现,包括常见集合类的可修改和不可修改版本。Kotlin的泛型实现与Java很像。下表列出了Kotlin支持的接口,以及创建Kotlin运行时库中实现了这些了接口的类的实例时,必须调用的函数:
接口描述用于创建实例的函数 List为不可变列表提供方法listOf MutableList为可变列表提供方法mutableListOf Set为不可变集提供方法setOf MutableSet为可变集提供方法mutableSetOf Map为不可变映射提供方法mapOf MutableMap为可变映射提供方法mutableMapOf
有关每种类型包含的各种方法和属性的详尽说明,请参阅Kotlin文档的API参考(API Reference)部分。下面通过示例说明上表列出的一些类型的方法,先看来一个不可变列表:
val someImmutableInts: List<Int> = listOf(10, 20, 3println("$someImmutableInts --> ${someImmutableInts.size} elements")
这个代码片段打印[10, 20, 30] --> 3 elements。Kotlin接口List的其他方法包括contains()、indexOf()、isEmpty()、lastIndexOf()和subList()。接口List还包含各种函数式编程函数,这些函数被称为扩展函数。扩展函数将在下一章介绍。
val mutableDoubles: MutableList<Double> = mutableListOf(3.14, 1.0,25.5)mutableDoubles.add(1, -1.99)mutableDoubles.removeAt(0)println(mutableDoubles)
上述代码打印 [-1.99, 1.0, 25.5]。接口MutableList的其他常用函数包括addAll()、clear()和remove(),它们用于删除特定的元素。
val mapNumbers: Map<String, Int> = mapOf("one" to 1, "ten" to 10,"thirty" to 30)println(mapNumbers["thirty"])for ((key, value) in mapNumbers) {print("$key = $value ")}println()
这个示例打印30和one = 1 ten = 10 thirty = 30。Map的其他常用方法包括keys()、values()、containsKey()、containsValue()、getOrDefault()(1.1版新增的)和isEmpty()。
9.3.4 循环
Kotlin支持所有常见的循环语句,如for、while和do…while。
首先来看一个for循环示例:
val items = listOf(10, 20, 30)for (i in items) {println(i)}
一点都不出乎意料。这段代码打印10、20和30,每个元素各占一行。
还有while语句。与其他语言中一样,while语句先检查条件,如果为false,就不做任何迭代,否则就开始循环,直到条件为false或调用了方法break:
var x = 10while (x > 20) {println("Hello")x++}
这个示例什么都不打印,因为x不比20大。
另外,还有变种do…while:
var y = 0do {y++if (y == 2)continueprintln(y)} while (y % 5 != 0)
这段代码打印1、3、4、5。
与Java和众多其他流行的编程语言一样,所有Kotlin循环结构都支持break和continue语句,它们分别停止迭代和跳过当前迭代。
请注意,原始字符串不支持字符串模板。