语言特点

Kotlin可以编译成Java字节码,也可以编译成JavaScript字节码,方便在没有JVM的设备上运行。

基本语法

Kotlin语言是严格区分大小写的。

变量声明

Kotlin中定义一个变量,只允许在变量前声明两种关键字: valvar

const valval的区别

  • val: 相当于Java中的private final static
  • const val: 相当于Java中的public final static

定义常量时,优先考虑使用const val

空值处理

安全调用符?.,专门用于调用可空类型变量中的成员方法或属性,其语法格式为变量?.成员。其作用是判断变量是否为null,如果不为null才调用变量的成员方法或者属性。

使用?.调用可空变量的属性时,如果当前变量为空,则程序编译也不会报错,而是返回一个null值。

Kotlin中提供了一个Elvis操作符?:,通过Elvis操作符?:, 其语法格式为表达式?:表达式。如果左侧表达式非空,则返回左侧表达式的值,否则返回右侧表达式的值。当且仅当左侧为空时,才会对右侧表达式求值。

通过非空断言(!!.)来调用可空类型变量的成员方法或属性。使用非空断言时,调用变量成员方法或属性的语法结构为变量!!.成员。非空断言!!.会将任何变量(可空类型变量或者非空类型变量)转换为非空类型的变量,若该变量为空则抛出异常。

数据类型

Kotlin完全抛弃了Java中的基本数据类型,全部使用对象类型。Kotlin语言中的数据类型不区分基本数据类型和引用数据类型,分别为数值型、字符型、布尔型、数组型、字符串型。

Kotlin中每个字符类型变量都会占用2个字节。在给Char类型的变量赋值时,需要用一对英文半角格式的单引号' '把字符括起来。

数组是用Array表示,其中数值类型、布尔类型、字符类型都有数组的表现形式

这些数组类型变量的初始化有两种方式,一种是以数据类型ArrayOf()方法进行初始化,另一种是以arrayOf()方法进行初始化。

1
2
3
4
5
// 声明数组变量的两种方式
val intArray = intArrayOf(1, 2, 3)
val intArray2 = arrayOf(1, 2, 3)
println(intArray.contentToString())
println(intArray2.contentToString())

不能使用stringArrayOf()方法创建字符串类型数组,因为String不属于基本数据类型。要想在Kotlin中声明字符串数组,需要使用Array<String>,并且对应的初始化数组的方法也相应变成了arrayOf(),这种初始化方式对于其他类型的数组同样适用。

运算符

在进行取模(%)运算时,运算结果的正负取决于被模数(%左边的数)的符号,与模数(%右边的数)的符号无关。例如(-1)%2=-1,而1%(-2)=1

&&当运算符左边的表达式为false时,运算符右边的表达式不会进行运算,结果为false,因此&&被称作短路与。

同与操作类似,||表示短路或,当运算符||的左边为true时,右边的表达式不会进行运算,结果为true

字符串

字符串是不可变的,字符串中的元素可以使用索引的形式进行访问:即“变量名+角标”的形式,如str[i], 也可以用for循环遍历字符串.

为了方便字符串的查找,提供了多个函数,如first()last()get(index),分别用于查找字符串中的第1个元素、最后1个元素以及角标为index的元素。

1
2
3
4
val s = "Hello, World"
println(s.first())  // H
println(s.last())   // d
println(s[4])      // o

字符串截取主要使用的是subString()函数和subSequence()函数,这两个函数都有重载函数(函数名相同,参数不同)

1
2
3
4
5
6
7
val s = "Hello, World"
println(s.substring(2, 5))   // 返回值类型为String
println(s.subSequence(2, 5)) // 返回值类型为CharSequence

// 输出为
// llo
// llo

split()函数还可以传入多个拆分符,多个拆分符中间只需用逗号分隔即可。返回类型为List<String>

1
2
3
4
5
val s = "www.baidu.com"
println(s.split("."))  // [www, baidu, com]

val s2 = "www.baidu.com/query"
println(s2.split(".", "/"))  //[www, baidu, com, query]

此Kotlin提供了trim()trimEnd()等多个函数,其中trim()用于删除字符串前面的空格,trimEnd()用于删除字符串后面的字符。

原生字符串是使用3对引号(""" """)把所有字符括起来,原生字符串可以有效地保证字符串中原有内容的输出,即使原生字符串中包含转义字符也不会被转义。

在原生字符串中,使用模板表达式输出$需要使用${'$'}

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fun main() {
    val s = """
        Hello\nWorld
        ${'$'}200
    """.trimIndent()
    println(s)
}
// 输出
// Hello\nWorld
// $200

区间

  • .. 两端都是闭区间[a, b], a<b
  • until左闭右开[a, b), a<b
  • downTo两端都是闭区间[a, b], a > b
  • step 步长
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// ..重写了运算操作符rangeTo
val s1 = 1..5
println(s1.joinToString(", "))
// until 是infix函数
val s2 = 1 until 5
println(s2.joinToString(", "))
// downTo 是infix函数
val s3 = 5 downTo 1
println(s3.joinToString(", "))
// 设置步长
val s4 = 1..10 step 2
println(s4.joinToString(", "))

// 输出
// 1, 2, 3, 4, 5
// 1, 2, 3, 4
// 5, 4, 3, 2, 1
// 1, 3, 5, 7, 9

// 使用in关键子判断一个元素属于一个区间

val x = 10
val y = 9
if (x in 1..y+1) {
    println("fits in range")
}

val list = listOf("a", "b", "c")

// 使用!in关键子判断一个元素不属于一个区间
if (-1 !in 0..list.lastIndex) {
    println("-1 is out of range")
}
if (list.size !in list.indices) {
    println("list size is out of valid list indices range, too")
}

数组

Kotlin中,为了方便获取数组的长度,提供了一个size属性,在程序中可以通过“数组名.size”的方式来获取数组的长度,即元素的个数。

在Kotlin中,如果创建的数组对象没有被初始化,则当访问数组中的元素时,程序会报错并提示数组对象必须初始化。脚下留心: 数组中的索引不能超出索引的范围

通过数组的withIndex()方法来遍历并打印数组中元素对应的角标和元素。

除了使用数组的indexOf()方法来查找指定元素中第1个元素的角标之外,还可以通过数组的indexOfFirst()方法来查找指定元素中第1个元素的角标。

通过数组的lastIndexOf()方法来查找,该方法中传递的参数就是需要查找的元素。

除了调用数组的lastIndexOf()方法来查找指定元素的角标之外,还可以通过数组的indexOfLast()方法来查找指定元素的最后一个角标

1
2
3
4
5
val a = arrayOf("a", "b", "c", "d")
val index = a.indexOfLast {
	it == "d"
}
println(index)  // 3

is!is操作符

用于检查一个变量是否为某个类型的对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
val obj ="xxx"
if (obj is String) {
    print(obj.length)
}

if (obj !is String) { // same as !(obj is String)
    print("Not a String")
} else {
    print(obj.length)
}

类型自动转换

大多数情况下,我们都不需要显示显示转换变量类型,编译器会根据is操作符号自动进行转换

1
2
3
4
5
fun demo(x: Any) {
    if (x is String) {
        print(x.length) // x is automatically cast to String
    }
}

使用when语句时,类型自动转换同样存在

1
2
3
4
5
when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

非安全的类型转换

当使用as(中缀infix操作符)进行类型转换失败时,会抛出异常,这样的转换被称为非安全的类型转换

1
val x: String = y as String

上面的例子中,当ynull时,会抛出异常

使用下面的形式可以解决这个问题

1
val x: String? = y as String?

安全的类型转换

使用as?进行类型转换时,在转换失败时会返回null,而不是抛出异常,这样的类型转换被称为安全的类型转换

1
val x: String? = y as String

包管理

默认导入的包

Kotlin中,默认情况下自动导入了下面的包

另外,根据使用的平台不同,额外还导入了如下包

添加导入的包

使用import关键字可以定义需要添加的包

1
2
3
4
5
// 导入单个	
import org.example.Message // Message is now accessible without qualification

// 导入多个
import org.example.* // everything in 'org.example' becomes accessible

使用as关键字可以重命名包导入的对象,解决冲突问题

1
import org.test.Message as testMessage // testMessage stands for 'org.test.Message'

函数

函数声明

函数使用fun关键字声明。对于参数,不仅必须声明其名称,还必须声明其类型,并且必须声明函数返回值的类型。函数的主体通常是一个代码块,用花括号括起来

函数返回值

Python相反,在函数末尾省略return不会隐式返回null;如果函数要返回null,则必须使用return null

如果一个函数不需要任何返回值,则该函数应该声明返回类型为Unit,类似于Java中的void。当函数的返回值类型为Unit时,可以省略不写Unit。(或者根本不声明返回类型,在这种情况下,返回类型默认为Unit)。在这样的函数中,可能根本没有return 语句,或只有return

Unit是一个单例对象(在Python 中也恰好是None),也是该对象的类型,它表示此函数不会返回任何信息

vararg可选参数

使用关键字vararg,函数可以接受任意数量的参数,类似于Python中的 *args,但它们必须都属于同一类型。与Python不同的是,可以在可变参数之后声明其他位置参数,但最多可以有一个可变参数。

1
2
3
4
5
6
7
8
fun countAndPrintArgs(vararg numbers: Int) {

	println(numbers.size) // 输出 3

	for (number in numbers) println(number) // 输出 1,2,3

}
countAndPrintArgs(1, 2, 3)

可以使用包含所有可变参数的一个数组而不是列表或任何其他可迭代对象)来调用可变参数函数,使用*运算符(与 Python 相同的语法)将数组展开:

1
2
val numbers = listOf(1, 2, 3)
countAndPrintArgs(*numbers.toIntArray())

命名参数

Kotlin中没有**kwargs,但是可以定义具有默认值的可选参数,并且在调用函数时可以选择命名部分或所有参数(无论它们是否具有默认值)。具有默认值的参数仍必须明确指定其类型。像在Python中一样,已命名的参数可以在调用时随意重新排序:

1
2
3
4
5
6
fun foo(decimal: Double, integer: Int, text: String = "Hello") {
	TODO("Implement this method")
}

foo(3.14, text = "Bye", integer = 42)
foo(integer = 12, decimal = 3.4)

函数默认值

Python中,默认值的表达式只在函数定义时计算一次。这导致了一个经典的陷阱,当开发人员希望每次调用没有传递numbers参数的函数时,都得到一个新的空列表,但是实际上每次都使用相同的列表:

1
2
3
4
5
6
7
def tricky(x, numbers=[]): # Bug:每次调用都会使用相同的列表!
	numbers.append(x)
	print(numbers)

tricky(1)
tricky(2)
tricky(3)

依次输出

1
2
3
[1]
[1, 2]
[1, 2, 3]

Kotlin中,每次调用函数时,都会计算默认值的表达式。因此,只要使用在每次求值时生成新列表的表达式,就可以避免上述陷阱

1
2
3
4
5
6
7
8
fun tricky(x: Int, numbers: MutableList<Int> = mutableListOf()) {
    numbers.add(x)
    println(numbers)
}

tricky(1)
tricky(2)
tricky(3)

输出为

1
2
3
[1]
[2]
[3]

函数的简化写法

当一个函数中只有一行代码时,Kotlin允许我们不写函数体,直接将唯一的一行代码写在函数定义的尾部,中间等号连接即可。 如:

1
2
3
fun largerNumber(num1: Int, num2: Int): Int {
  return max(num1, num2)
}

可以直接写成如下形式,其中return关键字也省略掉了,等号足以表达返回值的意思

1
fun largeNumber(num1: Int, num2: Int): Int = max(num1, num2)

由于自动推导机制,我们知道max函数会返回Int类型,因此上面的代码还可以简化成

1
fun largerNumber(num1: Int, num2: Int) = max(num1, num2)

函数重载

Python中,函数名称在模块或类中必须唯一。而在Kotlin中,可以重载函数,即可以有多个具有相同名称的函数声明。

重载的函数必须通过其参数列表相互区分(参数列表的类型与返回类型一起被称为函数签名,但是返回类型不能用于消除重载函数的歧义)。例如,可以在同一个文件中同时声明这两个函数:

1
2
fun square(number: Int) = number * number
fun square(number: Double) = number * number

在调用时,要使用的函数取决于参数的类型:

1
2
square(4) // 调用第一个函数;结果为 16 (Int)
square(3.14) // 调用第二个函数;结果为 9.8596 (Double)

尽管此示例恰好使用相同的表达式,但这不是必须的。如果需要,重载的函数可以做完全不同的事情(尽管可以使行为截然不同的函数互相重载,但是代码可能会造成混乱)。

TODO函数

Kotlin标准库中提供了一个TODO函数用于标记没有完成的代码,调用时会自动抛出NotImplementedError异常。并且它的返回值是Nothing类型,所以使用它可以不考虑返回值的类型

1
fun calcTaxes(): BigDecimal = TODO("Waiting for feedback from accounting")

中缀函数infix

在创建map集合的时候,我们用到了to这样的语法

1
2
3
4
val fruits = mapOf("Apple" to 1, "Orange" to 2, "Banana" to 3)
for ((name, index) in fruits) {
	println("$name => $index")
}

首先to并不是Kotlin中的关键字,之所以能够使用A to B这样的语法结构,是因为Kotlin提供了一种高级语法糖特性: infix函数。 infix函数并不是什么难理解的事物,它只是把编程语言的调用语法规则调整了一下而已,如果A to B这样的写法,实际上等价于A.to(B)infix函数允许我们将函数调用时的小数点,括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编程。

1
2
3
4
5
6
7
infix fun String.beginWith(s: String) = startsWith(s)

fun main() {
    // 我们即可以向普通函数一样使用,也可以使用infix函数的语法糖特性
    println("Apple".beginWith("A"))
    println("Apple" beginWith "A")
}

使用infix函数有3个比较严格的条件

  1. infix函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式来将它定义到某个类当中。
  2. infix函数必须接收且只能接收一个参数,至于参数类型是没有限制的。
  3. 参数不能是可变参数,并且要没有默认值

只有同时满足这3点,infix函数的语法糖才具备使用条件。

函数扩展

不少现代的高级编程语言中都有扩展函数这个概念,Java却一直都不支持,但是Kotlin对扩展函数进行了很好的支持。

什么是扩展函数 ?

扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。

扩展函数的语法结构为

1
2
3
fun ClassName.methodName(param1: Int, param2: Int): Int {
	return 0
}

相比与定义一个普通的函数,定义扩展函数只需要在函数名的前面添加一个ClassName.的语法结构,就表示将该函数添加到指定的类中了。这里的ClassName称为receiver type

比如,向String类中添加一个统计字符串中包含字母个数的函数。

1
2
3
4
5
6
7
8
9
fun String.lettersCount(): Int {
    var count = 0
    for (char in this) {
        if (char.isLetter()) {
            count++
        }
    }
    return count
}

这里需要注意,我们将lettersCount()函数定义为String类的扩展函数,那么函数中就自动拥有了String实例的上下文。因此lettersCount()函数就不再需要接收一个字符串参数了,而是直接使用this即可,因为现在this就代表着字符串本身。

然后我们就可以像调用String类自带的函数一样调用它了

1
val count = "ABC#$$123EFG".lettersCount()

最佳实践

当我们希望向String类中添加一个扩展函数时,建议先先创建一个String.kt文件。文件名虽然没有固定的要求,但是建议向哪个类添加扩展函数时,就定义一个同名的Kotlin文件,这样便于后期查找。

当然,扩展函数也是可以定义在任何一个现有的类当中的,并不一定要创建新的文件。不过通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。

扩展函数是静态扩展

调用扩展函数时,是根据声明时的类型来决定调用方法的,而不是实际运行时的类型,例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
open class Shape

class Rectangle: Shape()

fun Shape.getName() = "Shape"

fun Rectangle.getName() = "Rectangle"

fun printClassName(s: Shape) {
    println(s.getName())
}

// 这里传的参数虽然是Rectangle,
// 但是printClassName声明的参数类型是Shape
// 所以结果仍然是Shape
printClassName(Rectangle()) // Shape

扩展函数无法覆盖原始类里已有的同名函数

当扩展函数的名称和原始类的名称一样时,始终会调用原始类里的函数

1
2
3
4
5
6
7
class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType() { println("Extension function") }

Example().printFunctionType() // Class method

当然,在扩展函数里对原始类的函数重载是可以的

1
2
3
4
5
6
7
class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType(i: Int) { println("Extension function") }

Example().printFunctionType(1) // Extension function

Nullable receiver

扩展函数的receiver同样可以为空.

1
2
3
4
5
6
fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 'this' is autocast to a non-null type, so the toString() below
    // resolves to the member function of the Any class
    return toString()
}

扩展属性

除了扩展函数之外,同样支持扩展属性。

1
2
val <T> List<T>.lastIndex: Int
    get() = size - 1

不过由于扩展属性同样不能修改原始类的代码,因此扩展属性只能通过gettersetter

方法来实现,没法直接扩展

例如,下面的写法就是错误的

1
val House.number = 1 // error: initializers are not allowed for extension properties

伴生对象扩展(Companion object extensions)

如果一个类里面包含伴生类,那么伴生类同样是可以扩展的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class MyClass {
    companion object { }  // will be called "Companion"
}

// 注意伴生对象的recevier写法
fun MyClass.Companion.printCompanion() { println("companion") }

fun main() {
    MyClass.printCompanion()
}

将扩展函数作为类成员定义

一般情况下,我们是直接在包名顶级下定义扩展函数的。同样也可以在类里面定义扩展函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}

class Connection(val host: Host, val port: Int) {
     fun printPort() { print(port) }

     fun Host.printConnectionString() {
         printHostname()   // calls Host.printHostname()
         print(":")
         printPort()   // calls Connection.printPort()
     }

     fun connect() {
         /*...*/
         host.printConnectionString()   // calls the extension function
     }
}

fun main() {
    Connection(Host("kotl.in"), 443).connect()
    //Host("kotl.in").printConnectionString(443)  // error, the extension function is unavailable outside Connection
}

运算符重载

我们可以对+, -, *,/等运算进行重载,实现将任意两个对象进行相加,相减等操作。

运算符重载使用的是operator关键字,只要在指定的函数前面加上operator关键字,就可以实现运算符的重载了。指定的函数指的是呢?不同的运算符对应的指定函数是不一样,比如加号运算符对应的是plus()函数,减号运算符对应的是minus()函数。具体运算符和实际对应的函数关系如下表:

运算符和函数关系表

如果想实现让两个类相加的功能,那么它的语法结构为

1
2
3
4
5
class Obj {
	operator fun plus(obj: Obj): Obj {
		//处理相加的逻辑
	}
}

上述结构中,关键字operator和函数名plus都是固定不变的,而接收的参数和函数返回值可以根据实际逻辑自行设定。上述代码就表示一个Obj对象与另外一个Obj对象相加,最终返回一个新的Obj对象。对应的调用方式如下:

1
2
3
val obj1 = Obj()
val obj2 = Obj()
val obj3 = obj1 + obj2

obj1 + obj2这种语法看上去很神奇,但其实就是Kotlin给我们提供的一种语法糖,它会在编译的时候转换成obj1.plus(obj2)的调用方式。

实际例子

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Money(val value: Int) {
    operator fun plus(money: Money): Money {
        return Money(value + money.value)
    }

    operator fun plus(newValue: Int): Money {
        return Money(value + newValue)
    }
}

fun main() {
    val m1 = Money(100)
    val m2 = Money(50)
    println((m1 + m2).value)
    println((m1 + 20).value)
}

上面的例子,通过函数重载和运算符重载,不仅实现了两个Money对象直接相加,也可以让Money对象和一个Int对象直接相加。

结合扩展函数和运算符重载(this作用域声明)

实现一个重复字符串乘以数字的效果,类似python中的str * number

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
operator fun String.times(n: Int): String {
    val outerThis = this // 这里的this表示String对象。将字符串this对象的引用赋值给一个变量,便于在下面引用
    return StringBuilder().run {
        repeat(n) {
          // append(this) // 这里的this表示StringBuilder()对象,所以这里不能用this,
            append(outerThis)
        }
        toString()
    }
}

fun main() {
    val a = "abc"
    val s = a * 2
    println(s) // 输出abcabc
}

上面的例子中用了一个临时变量来保存外部的String对象的引用。实际上在Kotlin中有更简单的写法,直接通过this@label的方法指明当前的this属于哪个作用域。上面的例子即是this@times

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
operator fun String.times(n: Int): String {
    return StringBuilder().run {
        repeat(n) {
            append(this@times)
        }
        toString()
    }
}

fun main() {
    val a = "abc"
    val s = a * 2
    println(s) // 输出abcabc
}