9.3 Kotlin语言基础

学习Kotlin时,建议你随时参阅Kotlin参考文档。要找到这个文档,可单击Kotlin官网主页中的链接LEARN,也可直接访问http://kotlinlang.org/docs/reference/

本节介绍如下主题:

  • 定义局部变量;
  • 定义函数;
  • Kotlin类型;
  • 循环。

9.3.1 定义局部变量

要定义局部变量,可使用varval

  1. var aMutableNumber = 24
  2. val anImmutableNumber = 42

区别在于使用var定义的变量是可以修改的,而使用val定义的变量是不可修改的。定义变量时还可指定其类型:

  1. var aMutableString: String = "A type can optionally be specified..."
  2. val anImmutableString: String = "...no matter whether you are using
  3. var or val"

支持的类型将在9.3.3节讨论。要将null赋给变量,务必采取必要的预防措施。Kotlin的类型系统比较独特,针对可为null的变量制订了一些使用规则,这也将在9.3.3节讨论。

变量可在函数或类中定义,也可在代码开头定义(采用过程性编程时),这些情形都将在相关的章节中讨论。

你可按下面的规则使用字面量:

  • 不带后缀的整数为Int
  • 要使用Long字面量,必须指定后缀L;
  • 可使用十六进制表示IntLong,为此可加上前缀0x;
  • 可使用二进制表示整数,为此可加上前缀0b;
  • 不带后缀的浮点数为Double
  • 带后缀f或F的浮点数为Float
  • FloatDouble值都可使用科学记数法表示。

下面是一些示例:

  1. val thisIsAnInt = 42
  2. val thisIsALong = 1000L
  3. val hexInt = 0xFF
  4. val binaryLong = 0b10101100L
  5. val thisIsADouble = 149.16
  6. val thisIsAFloat = 501.19e2f

Kotlin不像Java那样使用关键字new来实例化对象,而像使用函数那样使用类名:

  1. class A (i: Int) {
  2. }
  3. val a = A(25)

上述代码定义了类A,它包含接受一个Int输入参数的主构造函数。接下来,实例化了这个类:在类名后加上构造函数参数的值。

9.3.2 定义函数

要定义函数,可使用关键字fun

  1. fun functionName() {
  2. }

当然,也可给函数指定参数,但指定参数时必须指定其类型:

  1. fun functionNameWithParameters(i: Int, j: Int) {
  2. println(i * j)
  3. }

如果没有指定返回类型(就像前面两个示例那样),函数的返回类型默认为UnitUnit相当于Java中的void,不同之处在于可将Unit返回值赋给变量:

  1. fun noReturnValue(x: Int, y: Int): Unit {
  2. val f = noReturnValue(1, 2)
  3. println(f)

如果在REPL中输入并执行上述代码片段,将打印kotlin.Unit。下面是一个返回整数(Kotlin类型Int)的函数:

  1. fun returnsAnInt(x: Int, y: Int): Int {
  2. return x * y
  3. }
  4. val f = returnsAnInt(10, 10)
  5. println(f)

如果函数只包含一行代码,定义它时可不使用大括号({ }),而使用运算符=。对于只包含一行代码的函数,可不显式地指定返回类型,因为返回类型可根据代码推断出来:

  1. 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.Integerjava.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那样使用它:

  1. val s: String = "Hello!"

Kotlin也支持原始字符串,这种字符串可横跨多行:

  1. val s: String = """
  2. raw string"""

不同于常规字符串,在原始字符串中,可使用斜杠对字符进行转义(例如,\n表示换行符,\t表示制表符)。

还支持字符串模板:

  1. var favoriteBar = "FooBar"
  2. println("Your favorite bar's name $favoriteBar consists of
  3. ${favoriteBar.length} characters")

这将打印Your favorite bar's name FooBar consists of 6 characters

9.3 Kotlin语言基础 - 图1 请注意,原始字符串不支持字符串模板。

  • 安全地处理null

前面多次说过,Kotlin可避免将null赋给引用变量导致的错误。对于普通类型变量,如果你将null赋给它,Kotlin将拒绝编译。

例如,下面的代码无法通过编译:

  1. var currentTime = java.util.Date()
  2. // 下面这行代码无法通过编译
  3. currentTime = null

前面说过,实例化类时,Kotlin不要求你使用关键字new(Kotlin根本就不支持这个关键字)。如果你运行上述代码,将出现如下错误:

  1. error: null can not be a value of a non-null type Date

要让这些代码通过编译,必须在变量的类型名后面加上问号:

  1. var currentTime: java.util.Date? = java.util.Date()
  2. // 现在这行代码能够通过编译
  3. currentTime = null

要调用currentTime的方法或访问其其他成员,必须让编译器知道,这个引用可能为null,也可能不为null。例如,下面的代码无法通过编译:

  1. var currentTime: java.util.Date? = java.util.Date()
  2. // 下面这行代码无法通过编译
  3. var seconds = currentTime.getTime()

这将导致如下错误:

  1. error: only safe (?.) or non-null asserted (!!.) calls are allowed on a
  2. nullable receiver of type Date?

在上述代码片段中,虽然currentTime不是null引用,但你必须告诉Kotlin编译器,你知道currentTime有可能为null。解决方案有多种:

  • 添加条件检查;
  • 使用安全调用运算符?.
  • 使用Elvis运算符?:
  • 使用运算符!!。  

  • 方法1:添加条件检查

通过添加一条if语句,可告诉编译器,你知道引用变量有可能为null

  1. fun test() {
  2. var currentTime: java.util.Date? = java.util.Date()
  3. println("Line below will now compile fine")
  4. var seconds = if (currentTime != null) currentTime.getTime() else 0
  5. println(seconds)
  6. }
  7. test()

编译器得知你意识到这个实例变量可能为null后,就会心满意足地编译代码。以这种方式使用时,if条件返回currentTime.getTime()0。在前述示例中,调用test()时将打印currentTime.getTime()的输出,因为currentTime不是null引用。

请注意,仅当编译器知道其他线程无法访问这个变量时,这种做法才管用。由于currentTime是在函数中定义的,其他线程确实无法访问它。如果currentTime是一个类的公有字段,编译器依然会拒绝编译这些代码,因为在执行if(currentTime != null)检查之后到执行currentTime.getTime()调用之前,其他线程可能修改currentTime。在这种情况下,必须采用其他方法,否则编译器将拒绝编译,并显示一条错误消息。

  • 方法2:使用安全调用运算符?.

Kotlint提供了安全调用运算符?.(问号后面跟一个句点),它在引用为null时返回null,否则就调用指定的方法或访问指定的成员:

  1. var currentTime: java.util.Date? = null
  2. var seconds = currentTime?.getTime()
  3. println(seconds)

调用test()时,将打印null,因为执行currentTime?.getTime()时,Kotlin发现currentTime是一个null引用。如果currentTime指向一个java.util.Date实例,Kotlin将打印方法getTime()的输出。

运算符?.的一个优点是可以串接,下面是一个虚构的示例:

  1. member1?.member2()?.member3()

如果属性member1null引用或方法member2返回null,整个表达式的结果都将为null。如果member1member2的输出都不是null引用,将返回方法member3的输出。

  • 方法3:使用Elvis运算符?:

对于第一个示例中的if语句,可使用Elvis运算符?:进行改写,得到的代码更简洁:

  1. var currentTime: java.util.Date? = null
  2. var seconds = currentTime?.getTime() ?: -1
  3. println(seconds)

上述代码将返回-1。这是因为currentTime?.getTime()返回null(安全调用运算符?.返回null,因为currentTimenull引用),因此返回Elvis运算符?:后面的字面量-1。如果currentTime指向一个java.util.Date实例,上述代码将打印getTime()的输出。

  • 方法4:使用!!运算符

Kotlin官方文档指出,这个运算符就是为喜欢异常NullPointerException的人设计的。

通过在变量名后面加上运算符!!,可让Kotlin编译器完全忽略null安全系统。如果这个实例变量为null引用,而代码试图调用其方法或访问其成员,Kotlin将引发NullPointerException异常,就像Java在这种情况下的做法一样:

  1. fun test() {
  2. var currentTime: java.util.Date? = null
  3. println("Next line compiles, but throws exception when running")
  4. var seconds = currentTime!!.getTime()
  5. println(seconds)
  6. }
  7. test()
  • 转换

在Java中,编译器在确定不会降低精度时,将自动进行转换。例如,下面的做法在Java中是合法的:

  1. // Java代码
  2. int a = 1000;
  3. long b = a;

由于将int值存储到long变量中时,不会降低精度,因此Java自动将其转换为long。Kotlin不会自动转换变量,而要求程序员手动进行转换:

  1. val a: Int = 1000
  2. val b: Long = a.toLong()

为支持转换,Kotlin的每种数值类型(IntLong等)都包含如下方法:

  • 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)部分。下面通过示例说明上表列出的一些类型的方法,先看来一个不可变列表:

  1. val someImmutableInts: List<Int> = listOf(10, 20, 3
  2. println("$someImmutableInts --> ${someImmutableInts.size} elements")

这个代码片段打印[10, 20, 30] --> 3 elements。Kotlin接口List的其他方法包括contains()indexOf()isEmpty()lastIndexOf()subList()。接口List还包含各种函数式编程函数,这些函数被称为扩展函数。扩展函数将在下一章介绍。

  1. val mutableDoubles: MutableList<Double> = mutableListOf(3.14, 1.0,
  2. 25.5)
  3. mutableDoubles.add(1, -1.99)
  4. mutableDoubles.removeAt(0)
  5. println(mutableDoubles)

上述代码打印 [-1.99, 1.0, 25.5]。接口MutableList的其他常用函数包括addAll()clear()remove(),它们用于删除特定的元素。

  1. val mapNumbers: Map<String, Int> = mapOf("one" to 1, "ten" to 10,
  2. "thirty" to 30)
  3. println(mapNumbers["thirty"])
  4. for ((key, value) in mapNumbers) {
  5. print("$key = $value ")
  6. }
  7. println()

这个示例打印30one = 1 ten = 10 thirty = 30Map的其他常用方法包括keys()values()containsKey()containsValue()getOrDefault()(1.1版新增的)和isEmpty()

9.3.4 循环

Kotlin支持所有常见的循环语句,如forwhiledo…while

首先来看一个for循环示例:

  1. val items = listOf(10, 20, 30)
  2. for (i in items) {
  3. println(i)
  4. }

一点都不出乎意料。这段代码打印102030,每个元素各占一行。

还有while语句。与其他语言中一样,while语句先检查条件,如果为false,就不做任何迭代,否则就开始循环,直到条件为false或调用了方法break

  1. var x = 10
  2. while (x > 20) {
  3. println("Hello")
  4. x++
  5. }

这个示例什么都不打印,因为x不比20大。

另外,还有变种do…while

  1. var y = 0
  2. do {
  3. y++
  4. if (y == 2)
  5. continue
  6. println(y)
  7. } while (y % 5 != 0)

这段代码打印1345

与Java和众多其他流行的编程语言一样,所有Kotlin循环结构都支持breakcontinue语句,它们分别停止迭代和跳过当前迭代。