Kotlin基础

数据类型

在kotlin中,变量使用var关键字声明,常量使用val关键字声明

变量声明可以通过变量名:数据类型的形式显式声明,也可以通过自动推导的方式声明。

1
2
3
4
5
6
//通过【变量名:数据类型】的形式声明变量
var name: String = "Soria"
val age: Int = 18
//可以不显式声明数据类型,可以自动推导
var height = 1.75
var weight = 55

Kotlin支持八种基本数据类型:ByteShortIntLongFloatDoubleCharBoolean

  1. 数字类型

    数字类型包括ByteShortIntLongFloatDouble

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    val byteNum: Byte = 127
    val shortNum: Short = 32767
    val intNum: Int = 2147483647
    val longNum: Long = 9223372036854775807L
    val floatNum: Float = 3.14F
    val doubleNum: Double = 3.141592653589793
    //无符号类型
    val uByteNum: UByte = 255u
    val uShortNum: UShort = 65535u
    val uIntNum: UInt = 4294967295u
    val uLongNum: ULong = 18446744073709551615u

    各个类型的取值范围如下所示:

    类型 位数 取值范围
    Byte 8 -128 ~ 127
    Short 16 -32768 ~ 32767
    Int 32 -2^31^ ~ 2^31^-1
    Long 64 -2^63^ ~ 2^63^-1
    Float 32 1.4^-45^ ~ 3.4028235^38^
    Double 64 4.9^-324^ ~ 1.7976931348623157^308^

    整型默认的数据类型为Int,浮点型为Double

    1
    2
    3
    4
    5
    6
    7
    8
    //以下数据类型均为Int类型
    val num1 = 1
    val num2 = 123
    val num3 = 123456
    //以下数据类型均为Double类型
    val num4 = 1.0
    val num5 = 123.456
    val num6 = 123456.789

    在Kotlin中,可以用”_”分隔数字,便于阅读。

    1
    val num = 1_000_000

    Kotlin支持二进制和十六进制表示,不支持八进制表示

    1
    2
    val binNum = 0b11110000
    val hexNum = 0x1f1e33
  2. boolean类型

    boolean类型只有truefalse两个值。

    1
    2
    val booleanTrue = true
    val booleanFalse = false

    boolean类型也可以通过关系运算得到。常用的关系运算符如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    val boolean1 = 12 > 10   //true
    val boolean2 = 12 == 10 //false
    val boolean3 = 12 != 10 //false
    val boolean4 = 12 <= 10 //false
    val boolean5 = 7 in 1..<10 //true [1,10)
    val boolean6 = 10 !in 1..10 //false [1,10]
    val boolean7 = 12 is Int //true
    val boolean8 = 7 < 10 && 12 > 10 //true
    val boolean9 = 7 < 10 || 12 > 10 //true
    val boolean10 =!true //false
  3. 字符和字符串类型

    Char类型是一个单一的unicode字符

    1
    2
    3
    4
    val char1 = 'a'   //a
    val char2 = '\u0061' //a
    val char3 = '\uFF21' //A
    val char4 = 'A'.code //65

    String类型通常用” “表示,也可以使用”””(模板字符串)多行表示。

    1
    2
    3
    4
    5
    6
    7
    8
    val str1 = "Hello World"
    val str2 = """
    Hello
    World
    Kotlin
    """.trimIndent() //trimIndent()用于清除缩进
    val str3 = "Kotlin"
    val str4 = "$str3 hello, world!${str3}" //Kotlin hello, world!Kotlin

转义字符

转义字符如下所示。

转义字符 说明
\t 制表符
\b 退格
\n 换行(LF)
\r 回车(CR)
\' 单引号
\“ 双引号
\\ 反斜杠
\$ 美元符

位运算

Kotlin提供了相关的位运算操作,仅适用于IntLong类型的变量。

操作符 操作说明
shl(num) 左移
shr(num) 右移
ushr(num) 无符号右移
and(bits) 按位与
or(bits) 按位或
xor(bits) 按位异或
inv() 取反

[!IMPORTANT]

不存在无符号左移。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//左移shl(num)
val num1 = 13 // 13 00001101
val num2 = num1 shl 1 // 26 00011010
//右移shr(num)
val num3 = 144 //144 10010000
val num4 = num3 shr 1 //72 01001000
//无符号右移ushr(num)
val num5 = -1 //-1 11111111 11111111 11111111 11111111
val num6 = num5 ushr 1 //2147483647 01111111 11111111 11111111 11111111
//按位与、或、异或(and、or、xor)
val num7 = 12 //12 00001100
val num8 = num7 and 0b00000110 //4 00000100
val num9 = num7 or 0b00000110 //14 00001110
val num10 = num7 xor 0b00000110 //10 00001000
//取反(inv)
val num11 = 127 //127 01111111
val num12 = num11.inv() //-128 10000000

流程控制语句

if-else和when

在Kotlin中,if语句与java类似,但是还提供了一种单行表达式的写法,类似于三元运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
val x = 10
//常规写法
if (x > 10){
println("x > 10")
} else {
println("x <= 10")
}
//单行表达式写法
//类似于 x == 10 ? true : false
if (x > 10) println("x > 10") else println("x <= 10")
//if的结果也可用于变量
val result = if (x > 10) "x > 10" else "x <= 10"
//多行代码块的结果默认以最后一行作为返回结果
val result2 = if (x > 10) {
println("1")
"x > 10"
} else {
println("2")
"x <= 10"
}

when类似于java中的switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
val x = 10
when (x) {
1 -> println("x = 1")
2 -> println("x = 2")
3 -> println("x = 3")
else -> println("x is not 1, 2, or 3")
}
//和if-else同理,也可以用一个变量接收when返回的值
val value = when {
x > 0 -> true
else -> false
}
//若某些值属于同一情况,则可以通过","合并
val value1 = when (x) {
1..3 -> "x is 1, 2, or 3"
in 4..5 -> "x is 4 or 5"
else -> "x is other value"
}
//若所有的情况都可以被说明,则可以省略else
val isTrue = true
val value2 = when (isTrue) {
true -> 1
false -> 0
}

for和while

for采用 for(x in array) 的形式,除此之外breakcontinue的用法同java。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val array = arrayOf(1, 2, 3, 4, 5)
for (i in array) {
println(i) // 1 2 3 4 5
}
for (i in 1..100 step 2) {
println(i) // 1 3 5 7 9... 99
}
//可以通过【标签@ + @标签】的形式,使多循环中的break或continue结束外层循环
xxx@ for (i in 40 downTo 1 step 20) {
for (j in 0..i step 10) {
if (i == j) break@xxx else println(j) // 0 10 20 30
}
println("Text after break") //该语句并不会被执行
}

whiledo-while语句同java。

普通函数

在Kotlin中,函数使用fun关键字声明。

Kotlin中的函数定义更加简洁和灵活。例如,我们可以在一个函数中定义另一个局部函数。

1
2
3
4
5
//执行foo(2)会返回4
fun foo(x: Int) {
fun multi(y: Int) = y * 2
println(multi(x))
}

函数的参数可以带有默认值,如果调用时不传入参数,则使用默认值作为传入的参数。我们也可以手动指定传入的参数对应哪一个形参。

1
2
3
4
5
6
7
8
9
10
11
12
fun sayHello() {
println("Hello")
}

fun add(a: Int, b: Int): Int {
return a + b
}
//直接调用sub()则返回5
//调用sub(b = 10)则返回0
fun sub(a: Int = 10, b: Int = 5): Int {
return a - b
}

如果一个函数较为简单,类似于上面的sub(),则可以采用如下方式简写。

1
2
3
fun multiply(a: Int, b: Int): Int = a * b
//求斐波那契数列第n项,最基础的实现方式
fun fibonacci(n: Int): Int = if (n in 1..2) 1 else fibonacci(n - 1) + fibonacci(n - 2)

然而使用上述方法求解斐波那契时,存在了大量的重复运算。通过使用tailrec关键字对函数进行修饰,我们可以进行尾递归优化。

1
2
3
4
5
6
7
// 1 1 2 3 5 8 13 21 34 55 89 144
//执行fibonacci(1, 1, 9)返回89
tailrec fun fibonacci(a: Int, b: Int, n: Int): Int {
return if (n == 0)
a
else
fibonacci(b, a + b, n - 1)

[!NOTE]

如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作。

tailrec就是让递归变成了迭代,因此tailrec关键字只能优化尾递归算法,其它递归算法无法优化。

Kotlin支持在缺省函数名的情况下,直接定义一个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun calculate(num: Int, condition: (Int) -> Boolean): Int {
return if (condition(num)) {
num * 2
} else {
num / 2
}
}

fun main() {
//此处使用匿名函数
calculate(5, fun(num:Int):Boolean{
return num in 1..10
})
}

函数的重载同java。

高阶函数

函数类型变量

在Kotlin中,函数类型的格式非常简单。举个例子:(Int) -> Unit

函数类型的声明需要遵循以下几点:

  1. 通过->组织参数类型和返回值类型。左边是参数类型,右边是返回值类型。
  2. 必须用()包裹参数类型。
  3. 返回值类型即使是Unit也必须显式声明。

如果一个函数类型没有参数,则可以通过() -> Unit表示。如果有多个参数,则通过”,”进行分隔:(Int, String) -> Unit

如果一个参数是可选的,则可以用”?”来表示。如果该函数类型的变量也是可选的,则可以将整个函数类型变为可选。

1
2
3
4
//msg在某种情况下不需要传入,则可以如下表示
val a: (code: Int, msg: String?) -> Unit
//整个函数类型也可以变为可选
val b:((code: Int, msg: String)->Unit)?

函数类型的变量还可以返回另一个函数。

1
2
3
4
5
6
//这表示传入一个Int类型的参数,返回一个类型为(Int) -> Unit的函数
val c: (Int) -> (Int) -> Int = { x -> { y -> x + y } }
//下面则表示传入一个函数类型的参数,返回Unit
val d: ((Int) -> Int) -> Int = { f -> f(2) }
//当然还可以接着套娃
val e: (Int) -> ((Int) -> Int) -> Int = { x -> { y -> x + y(x) } }

Lambda表达式

在Kotlin中,可以通过Lambda表达式简化匿名函数。

1
2
3
4
//执行println(sum(1, 2))会返回3
val sum: (Int, Int) -> Int = { a, b -> a + b }
//如果不需要使用某个变量,可以用_代替
val sum2: (Int, Int) -> Int = { _, b -> b }

我们可以直接把Lambda作为参数传入,作为实参使用。下述情况被称为尾随Lambda表达式,只有在最后一个参数是函数类型的情况下才可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun test(func: (Int) -> String) {
println(func(10))
}

fun test2(num:Int,func: (Int) -> String){
println(func(num))
}
//以下均会打印"收到的参数:10"
fun main() {
test({ "收到的参数:$it" })
//如果函数的最后一个形参是函数类型,可以直接写在括号后面
test(){"收到的参数: $it"}
//此时()内不存在参数,因此可以省略()
test{"收到的参数: $it"}
//如果在上述前提下,()还有其它参数,则只能如下表示
test2(10){ "收到的参数: $it" }
}

如果函数调用的是尾随Lambda表达式,默认的标签名为函数名。

1
2
3
4
5
6
7
8
9
10
11
fun printName(func: (Int) -> String){
println(func(12))
}

fun main() {
printName{
if(it > 5) return@printName "提前返回的结果"
println("正常情况")
"收到的参数: $it"
}
}

在Lambda中不能使用return返回结果。

内联函数

在Kotlin中使用Lambda表达式会带来一些额外的开销。我们可以通过inline关键字修饰函数,使其成为内联函数。被inline修饰的函数,在执行时会将函数体嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行时的时间开销。

1
2
3
4
5
6
7
8
fun main() {
test { println(it) }
}

inline fun test(func: (String) -> Unit) {
println("这是一个内联函数")
func("inline function")
}

以上代码相当于:

1
2
3
4
5
fun main(){
println("这是一个内联函数") // test()第一行
val it = "inline function" // 函数内传入的参数
println(it) // 调用传入的函数
}

内联函数会导致编译后的代码变多,但是获得了性能上的提升。该操作对高阶函数有显著的效果,对于普通的函数即使使用内联函数也不会获得多少提升。

内联函数中的函数形参无法作为值给到变量,只能调用。

由于内联导致代码直接被搬运,所以Lambda中的return语句可以不带标签。

1
2
3
4
5
6
7
8
9
fun main(){
test{ return }
println("调用上述方法之后...")
}

inline fun test(func: (String) -> Unit){
func("Hello World")
println("调用内联函数之后...")
}

上述代码运行后不会输出任何内容。这种情况被称为非局部返回。因为Lambda中的return作用于整个main(),可以通过@标签更改其作用域来防止非局部返回。

为了避免带有return的Lambda参数产生破坏,可以用crossinline关键字修饰该参数。

1
2
3
4
5
6
7
8
9
10
//以下代码执行后将会打印"调用内联函数之后..."和"调用上述方法之后..."
fun main(){
test{ return@test }
println("调用上述方法之后...")
}

inline fun test(func: (String) -> Unit){
func("Hello World")
println("调用内联函数之后...")
}

一个函数一旦被定义为内联函数,边不能获取闭包类中的私有成员,除非将它们声明为internal

通过noinline关键字修饰参数可以使其不具有内联的效果。

中缀函数

通过infix关键字修饰的函数被称为中缀函数,在调用时可以通过A 中缀方法 B的形式调用,省略了”.”和”()”。中缀函数在使用时必须满足以下条件:

  • 必须是某个类型的扩展函数或者成员方法;
  • 只能有一个参数,参数不能有默认值;
  • 该参数不能是可变参数。
1
2
3
4
5
6
7
8
9
class Name(val name: String){
infix fun add(other: String) = Name(this.name + other)
}

fun main() {
val name1 = Name("John")
val name2 = name1 add "Doe"
println(name2.name) // JohnDoe
}

[!NOTE]

Kotlin通过vararg关键字定义函数中的可变参数,类似于java中的...。在java中可变参数必须是最后一个参数,而在Kotlin中没有这个限制。

我们还可以使用*来将外部变量作为可变参数的变量。

1
2
3
4
5
fun printLetter(vararg letters: String) = println(letters.joinToString())
printLetter("a", "b", "c") // "a, b, c"

val letters = arrayOf("a", "b", "c","d","e")
printLetter(*letters) // "a, b, c, d, e"

Kotlin核心

类和初始化

Kotlin中的类和java类似。

通常,类中一般具有一些属性。在Kotlin中,可以通过constructor关键字添加属性。

1
class Student constructor(name: String, age: Int) {}

如果主构造函数没有任何注释或可见性修饰符,则可以省略constructor,如果类中没有内容,则可以省略{}

1
class Student(name: String, age: Int)

上述仅仅定义了构造函数的参数,还不是类的属性。可以在上述参数前添加var或者val来表示这个属性是可变还是不可变的。

1
class Student(var name: String, val age: Int)

一个类可以有一个主构造函数和多个次构造函数。次构造函数中可以自定义函数体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Student(var name: String, var age: Int){
constructor(name: String): this(name, 24)
constructor(age: Int): this("Unknown", age){
println("Age is $age")
}
}

fun main() {
val student1 = Student("Soria", 18)
println("${student1.name}: ${student1.age}") // Soria:18
val student2 = Student("John")
println("${student2.name}: ${student2.age}") // John:24
val student3 = Student(16) // Age is 16
}

在Kotlin中,可以通过init关键字进行初始化,类似于java中的static。但在Kotlin中,可以存在多个init

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
class Student(var name: String, var age: Int) {
init {
println("学生对象被创建了${name}")
}

init {
println("学生对象被创建了${age}")
}

init {
println("学生对象被创建了${name}:${age}")
}
constructor(name: String):this(name, 24) {
println("次构造函数执行...")
}
}

fun main(){
// 执行结果如下
// 学生对象被创建了ShewnGeung
// 学生对象被创建了24
// 学生对象被创建了ShewnGeung:24
// 次构造函数执行...
val student4 = Student("ShewnGeung")
}

类的扩展

Kotlin提供了扩展类或接口的操作,来为某个类添加一些额外的函数或属性。

1
2
3
4
5
6
7
// 为Kotlin内置的String类添加一个test函数
fun String.text() = println(this)
fun main(){
val name = "Soria"
// 可以直接使用,就好像String类中真的有这个函数一样
name.text()
}

类的扩展是静态的,并不会修改它们原本的类,也不会将新成员插入到类中,仅仅是将我们定义的功能变得可调用。如果要扩展属性,只能明确定义一个gettersetter来创建扩展属性。

如果命名发生冲突时,需要特别处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class A{
fun hello(){
println("Hello from A")
}
}

class B(private val a: A){
private fun hello(){
println("Hello from B")
}

private fun A.test(){
hello() // 优先匹配被扩展类中的函数
this.hello() // 扩展函数中的this依然指的是被扩展的类对象
this@B.hello() // 手动指定了B类中hello()
}
}

如果类本身就具有同名同参数的函数,那么扩展函数将失效。但如果通过扩展类实现函数的重载,完全没有问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test{
fun hello() = println("hello")
}

fun Test.hello() = println("hi") // 扩展失效

fun Test.hello(name: String) = println("hello $name") // 可以重载

fun main(){
Test().hello() // hello
Test().hello("Soria") // hello Soria
}

如果将一个扩展函数作为参数给到一个函数类型变量,需要在具体操作之前增加类型名称。

1
2
val len:String.() -> Int = { this.length }
println("soria".len())

运算符重载函数

Kotlin支持为程序中已知的运算符集提供自定义实现。要实现运算符重载,应为相应类型提供具有对应运算符指定名称的成员函数,当前的类对象直接作为运算符左边的操作数。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Student(val name: String, val age: Int) {
//重载"+"运算符
operator fun plus(other: Student): Student {
return Student(name + other.name, age + other.age)
}
}

fun main() {
val stu1 = Student("Tom", 20)
val stu2 = Student("Jerry", 21)
val stu3 = stu1 + stu2 // stu1.plus(stu2)
println("${stu3.name}: ${stu3.age}") // TomJerry: 41
}

运算符对应的名称是固定的。

下面是基本的一元运算符的固定名称。

符号 名称
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a++ a.dec()
a-- a.inc()

inc()dec()函数比较特殊,他们返回新生成的对象,将变量的值直接引用到这个对象。

例如,a++的操作步骤如下:

  • a的初始值存储到临时对象a0;
  • a0.inc()的结果分配给a;
  • 返回a0作为表达式的结果。

同理,++a的操作步骤如下:

  • a.inc()的结果分配给a;
  • 作为表达式的结果返回a的新值。

下面是基本的二元运算符的固定名称。

符号 名称
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a .. b a.rangeTo(b)
a ..< b a.rangeUntil(b)
a in b b.contains(a)
a !in b !b.contains(a)

对于in来说,返回值必须是Boolean类型。

运算符重载有可能会出现歧义。对于a = a + ba + = b来说,两者的功能基本一致,如果同时重载了plus()plusAssign(),编译器会不知道用哪个,因此会出现歧义。

1
2
3
4
5
6
7
operator fun plus(other: Student): Student {
return Student(name + other.name, age + other.age)
}
operator fun plusAssign(other: Student) {
name += other.name
age += other.age
}

上述两个运算符都能匹配到下方的使用,因此会报错。

赋值运算符多义性

比较运算符只需要实现一个即可。

运算符 名称
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

所有的比较都会转为compareTo()进行调用,返回Int类型的值用于判断是否满足条件。

1
2
3
4
5
6
7
8
operator fun compareTo(other: Student): Int {
return this.age.compareTo(other.age)
}
fun main(){
val stu1 = Student("Tom", 20)
val stu2 = Student("Jerry", 21)
println(stu1 >= stu2) // 20 - 21 < 0 返回false
}

Kotlin非常强大,甚至连()[]也可以重载。

运算符 名称
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, i_2, ..., i_n) a.invoke(i_1, i_2, ..., i_n)
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, i_2, ..., i_n] a.get(i_1, i_2, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, i_2, ..., i_n] = b a.set(i_1, i_2, ..., i_n)

空值和空类型

在Kotlin中,对空值处理非常严格。正常情况下我们不能直接给变量赋值为null

不能直接赋值null

可以通过在类型后面添加?使其变为可空类型,使该变量在初始情况下使用null

1
2
3
4
5
val name: String? = null
// 为了安全起见,需要对可空变量进行判断,然后才能正常使用
if(name != null){
println(name)
}

如果我们已经清楚该变量在某情况下一定不为空,我们可以通过非空断言操作符!!.来告诉编译器一定是安全的,可以执行。

1
2
val name: String? = null
println(name!!.length)

Kotlin提供了一种更安全的空类型操作,使用安全调用运算符?.来安全的使用对象,如果对象的属性为null则安全调用运算符返回null

1
2
3
4
val name: String? = null
// 如果str为null,得到的结果就是null,不会执行后面的操作
// 如果str不为null,则正常执行
println(name?.length)

如果想让安全调用运算符返回一个自定义的结果而不是null,可以使用Elvis运算符?:。如果Elvis左侧为null,则返回右侧的自定义结果。

Elvis运算符有点像三元运算符,但不是同样的作用。

1
2
val name: String? = null
val len: Int = name?.length ?: -1 // 如果name为null,则返回-1

解构

在使用对象时可能需要访问对象内部的一些属性。

1
2
val stu = Student("Soria", 18)
println("${stu.name}: ${stu.age}")

这样的操作不太优雅,我们可以通过解构的方式对属性进行处理。

要让一个类的属性支持解构,只需要添加约定的函数即可。在Kotlin中可以自定义解构出来的结果,通过定义componentN()并通过返回值的形式返回解构结果。

1
2
3
4
5
6
7
8
9
10
class Student(var name: String, var age: Int){
operator fun component1() = name
operator fun component2() = age
}

fun main(){
val stu = Student("Soria", 18)
val (a, b) = stu
println("${a}: ${b}")
}

访问权限

当我们指定一个类、方法或属性的修改或者重写权限时,需要用到限制修饰符。在Kotlin中,类的默认修饰符为final,因此类和方法默认是不可被继承或重写的。我们可以通过open修饰符使其可以被继承和重写。

修饰符 含义 与java比较
open 允许被继承或重写 相当于java类与方法的默认情况
abstract 抽象类或抽象方法 与java一致
final 不允许被继承或重写(默认情况) 与java主动指定final的效果一致
1
2
3
4
5
6
7
8
9
10
11
open class Bird {
open fun fly() {
println("I can fly.")
}
}

class Eagle : Bird() {
override fun fly() {
println("I can also fly.")
}
}

[!NOTE]

实际过程中应遵循里氏替换原则,子类可以扩展父类的功能,但不能改变父类原有的功能。包含以下四个设计原则:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法;
  • 子类可以增加自己特有的方法;
  • 子类的方法实现父类的方法时,形参应比父类的输入参数更宽松;
  • 子类的方法实现父类的抽象方法时,返回值要比父类更严格。

Kotlin还可以通过sealed关键字修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类无法继承这个类。

密封类是基于一个抽象类实现的,因此密封类不能被初始化

1
2
3
4
sealed class Bird {
open fun fly() = println("I can fly.")
class Eagle : Bird()
}

通过可见性修饰符可以指定类、方法及属性的可见性。Kotlin的可见性修饰符与java类似,但仍有几点不同:

  • Kotlin中默认修饰符为public,而java中是default
  • Kotlin可以在一个文件内单独声明方法及常量,同样支持可见性修饰符;
  • java中除了内部类以外,其他类不允许使用private修饰,Kotlin可以;
  • Kotlin中的protected修饰符只允许类和子类能访问,而java中的包也可以访问;
  • Kotlin中的独特修饰符internal,叫做模块内访问。被internal修饰的类,别人在引用我们的项目时不可访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 整个项目模块都可以访问该类,但别人用我们的项目作为第三方库时无法访问
internal abstract class School {
abstract val identify: String
protected val name: String = "School"
private val date: String = "2024-01-01"
}

private class Student : School() {
override val identify: String = "student"
init {
println(identify)
// name被protected修饰,允许其子类访问
println(super.name)
// date被private修饰,无法访问,此处会报错
println(super.date)
}
}

类的封装和继承

封装的目的是为了保证变量的安全性,使我们不必在意具体实现细节,只通过外部接口即可访问类的成员。如果不进行封装,类中的实例变量可以直接查看和修改,会给整个程序带来不好的影响。在编写类时,一般将成员变量私有化,外部类需要通过GetterSetter进行查看和设置变量。

1
2
3
4
5
6
class Student(private var name: String, private var age: Int) {
fun getName(): String { return name }
fun getAge(): Int { return age }
fun setName(name: String) { this.name = name }
fun setAge(age: Int) { this.age = age }
}

Kotlin中的继承和实现接口没有采用extendsimplements关键字,而是使用”:”来代替

如果父类存在一个有参构造函数,子类必须在构造函数中调用。如果父类存在多个构造函数,则可以任选一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
abstract class Person(val name: String, val age: Int) {
constructor(name: String) : this(name, 0)
abstract fun me()
}

class Student(name: String, age: Int, private val grade: Int) : Person(name, age) {
override fun me() = println("Student, $name, $age, in grade $grade.")
}

class Teacher(name: String, private val subject: String) : Person(name) {
override fun me() = println("Teacher, $name, teach $subject.")
}

fun main() {
val student1 = Student("John", 18, 12)
student1.me() // Student, John, 18, in grade 12.

val teacher1 = Teacher("Mary", "Math")
teacher1.me() // Teacher, Mary, teach Math.
}

重载

和java一样,在Kotlin中也是用override关键字来重写对象。但需要注意的是,被重写的对象需要用open关键字修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
open class Student() {
open fun study(subject: String) {}
}

class Soria:Student() {
override fun study(subject: String) {
println("Soria is studying $subject")
}
}

fun main() {
val soria:Student = Soria()
soria.study("Math") // Soria is studying Math
}

内部类

在Kotlin中声明内部类,必须用inner关键字修饰这个类,若没有修饰则会被视为嵌套类。

内部类包含对其外部类实例的引用,可以使用外部类中的属性。而嵌套类不包含,所以无法调用外部类的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class InnerClass{
val name = "ErrorInner"
// 这是嵌套类并非内部类
class ErrorInnerClass{
fun printName(){
// name变量无法被访问到
println(name)
}
}

val name2 = "CorrectInner"
// 正确的内部类
inner class CorrectInnerClass{
fun printName(){
// name变量可以被访问
println(name2)
}
}
}

一个类的内部可以定义多个内部类,每个内部类的实例都有自己独立的状态,与外部对象的信息相互独立。利用private修饰内部类,可以使其他类都不能访问内部类,具有良好的封装性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
open class Horse{
fun run() {
println("Horse is running fast.")
}
}

open class Donkey{
fun pack() {
println("Donkey can pack lots of things.")
}
}

class Mule{
private inner class HorseTest:Horse()
private inner class DonkeyTest:Donkey()

fun run() { HorseTest().run() }
fun pack() { DonkeyTest().pack() }
}

枚举类

Kotlin中的枚举和java相同,但是声明是要通过enum class的方式声明。

枚举类可以具有成员。

1
2
3
4
5
6
7
8
9
10
11
12
enum class LightState(val color: String) {
RED("红灯"),
YELLOW("黄灯"),
GREEN("绿灯");

fun isGreen() = this == GREEN
}

fun main() {
val light = LightState.GREEN
if (light.isGreen()) print("可以通行")
}

枚举类还提供了很多方法用于我们快速使用。

1
2
3
4
5
6
7
8
9
10
// 通过valueOf以字符串形式转化为对应名称的枚举
val state = LightState.valueOf("RED")
val state = enumValueOf<LightState>("RED")
// 枚举在第几个位置
state.ordinal
// 枚举名称
state.name
// 获取全部枚举,得到的结果是List的子接口EnumEntries类型,因此可以当做List使用
val entries = LightState.entries
val values: Array<LightState> = enumValues<LightState>()

数据类

我们可以通过data class声明一个数据类。声明一个数据类必须满足以下条件:

  • 必须拥有一个构造方法,该方法至少包含一个参数;
  • 构造方法的参数强制使用var或val进行声明;
  • data class前不能用abstract、open、sealed或inner进行修饰。

数据类既可以实现接口也可以继承类。

1
2
3
4
5
6
7
8
9
10
11
12
13
data class Animal(val name: String, val food: String){
fun eat(){
println("$name is eating $food.")
}
}

fun main() {
val animal = Animal("Lion", "Meat")
animal.eat()

val (nameP, foodP) = Pair("Sheep", "Grass")
println("$nameP is eating $foodP.")
}

Kotlin提供了PairTriple两个数据类,前者有两个属性,后者有三个,让使用者不必主动声明数据类。

抽象类

有时候我们设计的类仅仅是作为给其他类集成使用的类,其本身不需要创建任何实例对象。通过abstract关键字可以将一个类声明为抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
abstract class Person {
abstract val type: String
abstract fun me()
}

class Student : Person() {
override val type: String = "student"
override fun me() = println("I am $type.")
}

class Teacher : Person() {
override val type: String = "teacher"
override fun me() = println("I am $type, and I teach students.")
}

抽象类不仅可以具有抽象的属性,同时也具有普通类的性质,同样可以定义非抽象的属性或函数。同时,抽象类也可以继承自其他的类(可以是抽象类也可以是普通类)。

接口

接口只能包含函数或属性的定义,所有的内容默认用abstract修饰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface Student {
val name: String
fun study()
// 为了应对需求,接口也可以编写默认实现
// 默认情况为open,可以用private修饰掉
fun sleep() = println("I am sleeping")
}

class Person : Student {
override val name: String = "John"
override fun study() {
println("I am studying")
}
}

fun main() {
val person = Person()
person.study()
person.sleep()
}

伴生对象

通过companion object关键字可以创建伴生对象。伴生对象和java中static的修饰效果性质一样,且全局只有一个单例。伴生对象需要声明在类的内部,在类被装载时会被初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Prize {
companion object {
private val PRIZES = arrayOf("一等奖", "二等奖", "三等奖")
fun getPrize(index: Int): String {
return if (index in 1..3)
PRIZES[index - 1]
else
"未中奖"
}
}
}

fun main() {
// 一等奖
println(Prize.getPrize(1))
}

object单例

单例模式最大的特点是系统中只能存在一个实例对象,所以在java中必须设置构造方法私有化,以及提供静态方法创建实例的方式来创建实例对象。

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
public class DatabaseConfig {
private String host;
private int port;
private String username;
private String password;

private static DatabaseConfig databaseConfig = null;

private static final String DEFAULT_HOST = "127.0.0.1";
private static final int DEFAULT_PORT = 3306;
private static final String DEFAULT_USERNAME = "root";
private static final String DEFAULT_PASSWORD = "";

public DatabaseConfig(String host, int port, String username, String password) {
this.host = host;
this.port = port;
this.username = username;
this.password = password;
}

static DatabaseConfig getdatabaseConfig() {
if (databaseConfig != null) {
return databaseConfig;
}else {
return new DatabaseConfig(DEFAULT_HOST, DEFAULT_PORT, DEFAULT_USERNAME, DEFAULT_PASSWORD);
}
}
}

上面是java实现一个最基本单例模式的精简例子(省略了多线程以及多种参数创建单例对象的方法)。它依赖static关键字,同时还必须将构造方法私有化。

在Kotlin中,通过object关键字可以直接实现单例。object的全局声明对象只有一个,所以并不需要语法上的初始化,甚至不需要构造方法。

1
2
3
4
5
6
7
8
9
10
11
12
object DatabaseConfig {
var host = "127.0.0.1"
var port = 3306
var username = "root"
var password = ""
}

fun main() {
// 通过var修饰的属性,我们还可以修改它们
DatabaseConfig.host = "localhost"
DatabaseConfig.port = 3307
}

object还可以代替java的匿名内部类。例如,通过java对字符串列表进行排序:

1
2
3
4
5
6
7
8
9
10
11
12
13
List<String> list = Arrays.asList("apple", "banana", "orange");
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1 == null) {
return -1;
}
if (o2 == null) {
return 1;
}
return o1.compareTo(o2);
}
});

在Kotlin中,可以通过object表达式对其进行改善。

1
2
3
4
5
6
7
8
9
10
11
12
val list: List<String> = listOf("apple", "banana", "orange")
val comparator = object : Comparator<String> {
override fun compare(o1: String?, o2: String): Int {
if (o1 == null) {
return -1
} else if (o2 == null) {
return 1
}
return o1.compareTo(o2)
}
}
Collections.sort(list, comparator)

在java中,匿名内部类只能继承一个类以及实现一个接口,而object表达式没有这个限制。而且object表达式在运行时并不会全局只存在一个对象,而是每次运行时都会生成一个新的对象。

我们还可以将上面的代码通过Lambda表达式改造一下。

1
2
3
4
5
6
7
val comparator = Comparator<String> { o1, o2 ->
if (o1 == null)
return@Comparator -1
if (o2 == null)
return@Comparator 1
o1.compareTo(o2)
}

当只需要实现一个方法时,使用Lambda表达式更适合;当匿名内部类内有多个方法需要实现时,使用object表达式更加合适。

类型系统

可空类型

在Kotlin中,处理NPE(NullPointerException)非常容易。Kotlin能够很好的区分非空类型和可空类型。在Kotlin中访问非空类型的变量永远不会抛出空指针异常。

具体见上一章节。

通过Either代替可空类型

Either类型只有LeftRight两个子类型。通常来说Left代表出错的情况,Right代表正确的情况。如果Either[A, B]对象包含的是A的实例,它就是Left实例,否则就是Right实例。

在Kotlin中并没有Either类,但是可以通过密封类便捷的创造出Either类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sealed class Either<A, B> {
class Left<A, B>(val value: A) : Either<A, B>()
class Right<A, B>(val value: B) : Either<A, B>()
}
// 眼镜,眼镜一定有度数
data class Glasses(val degreeOfMyopia: Int)
// 学生,学生可能戴着眼镜
data class Student(val glasses: Glasses?)
// 座位,座位上可能有学生
data class Seat(val student: Student?)

fun getDegreeOfMyopia(seat: Seat?):Either<Error,Int>{
return seat?.student?.glasses?.let{ Either.Right(it.degreeOfMyopia)}?:Either.Left(Error("No student or glasses found"))
}

[!NOTE]

let的概念

1
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

调用某对象的let函数,该对象会作为函数的参数,在函数块内可通过it代指该对象。返回值为函数的最后一行或指定的return表达式。

按照上述写法代码量会变多,但是我们需要用ADT更好地组织业务。定义一个Error类,将所有步骤中的错误都抽象为不同的子类型,便于最终的处理以及后期排查错误。

类型检查

在Kotlin中,可以通过is关键字判断一个对象是什么类型。

1
2
3
if(obj is String){
println(obj.length)
}
1
2
3
4
when(obj){
is String -> println(obj.length)
!is String -> println("Not a String")
}

上述obj为Any类型,虽然做了类型判断,但是在没有类型转换的前提下,我们直接使用了length方法。这主要通过智能类型转换实现。

智能类型转换

智能类型转换时隐式完成的。

1
2
val stu:Student = Student(Glasses(10))
if(stu.glasses!= null) println(stu.glasses.degreeOfMyopia)

Kotlin和其他语言一样,无法直接将父类型转化为子类型。当类型需要强制转换时,可以通过as关键字实现。

1
2
3
4
5
6
7
8
open class Human data class Teacher(val name:String) :Human()
fun getTeacher():Human = Teacher("John")

val teacher = getTeacher() as Teacher
println(teacher.name)
// 也可以这么写
// val teacher = getTeacher()
// teacher as Teacher

as是不安全的类型转换,一般使用as?,如果对象为空则返回null

1
2
3
4
val teacher = getTeacher() as? Teacher
if(teacher != null){
println(teacher.name)
}

Any和Any?类型

Any类型

Any类型式非空类型的根类型。如果定义了一个没有指定父类型的类型,则该类型将是Any类型的直接子类型。如果为定义的新类型指定了父类型,则该父类型将是新类型的直接父类型,但新类型的最终根类型仍为Any

如果新类型实现了多个接口,那么将具有多个直接的父类型,但最终根类型还是Any

1
2
3
4
5
6
7
abstract class Animal(val weight:Double)

class Fish(weight:Double,val swimSpeed:Double):Animal(weight)

interface CanFly
interface BuildNest
class Bird(weight:Double,val flySpeed:Double):Animal(weight),CanFly,BuildNest

Kotlin把java方法参数和返回类型中的Object类型看作是Any(又或者是平台类型)。当Kotlin在函数中使用Any时,会被编译成java字节码中的Object。

[!NOTE]

平台类型本质上是Kotlin不知道可空性信息的类型。所有java引用类型都在Kotlin中表现为平台类型。当在Kotlin中处理平台类型的值的时候,它既可以被当做可空类型来处理,也可以被当做非空类型来处理。

Any?类型

Any?Any的父类型,而且**Any?类型是所有类型的根类型。**

Any?可以看作是Any∪null

自动装箱与拆箱

Kotlin没有int、float等原始类型,而是Int、Float等引用类型包装类。用java举例,Kotlin中的Int相当于intInt?相当于Integer

数组

在Kotlin中,数组是一个Array类型的对象。数组在创建完成之后,数组容量和元素类型是固定不变的,后续无法进行修改。数组在内存中是连续的,所以性能比较好。

1
2
3
4
5
val arr = arrayOf(1, 2, 3, 4, 5)
// 原生类型数组,对应java中的double[]、short[]等
val arr2 = doubleArrayOf(1.0, 2.0, 3.0, 4.0, 5.0)
val arr3 = shortArrayOf(1,2,3,4,5)
val arr4 = charArrayOf('a', 'b', 'c', 'd', 'e')

数组的打印

Kotlin提供了很多种打印数组的方式。

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
val arr = arrayOf(1, 2, 3, 4, 5)
// 基本的for循环
// 1,2,3,4,5
for (i in 0..<arr.size) { print("${arr[i]},") }
println("\b")
// 增强for循环
// 1,2,3,4,5
for (element in arr) { print("${element},") }
println("\b")
// 使用withIndex解构后可同时遍历索引和值
// 0 -> 1,1 -> 2,2 -> 3,3 -> 4,4 -> 5
for ((index, value) in arr.withIndex()) { print("$index -> $value,") }
println("\b")
// forEach高阶函数
// 1,2,3,4,5
arr.forEach { print("$it,") }
println("\b")
// forEachIndexed高阶函数,可以同时遍历索引和值
// 0 -> 1,1 -> 2,2 -> 3,3 -> 4,4 -> 5
arr.forEachIndexed { index, value -> print("$index -> $value,") }
println("\b")
// joinToString将数组转化为字符串的形式,默认用","隔开每个元素
// 1, 2, 3, 4, 5
println(arr.joinToString())
// 自定义joinToString返回的结果形式
// 2, 4, 6, 8, 10
println(arr.joinToString { (it * 2).toString() })
// 自定义joinToString的分隔符、前缀和后缀
// {1-2-3-4-5}
println(arr.joinToString(separator = "-", prefix = "{", postfix = "}"))
// 限制joinToString输出的元素个数为limit,并用truncated的内容表示剩余的元素
// 1, 2, ...
println(arr.joinToString(limit = 2, truncated = "..."))

多维数组

多维数组与java类似。

1
2
3
4
5
val arr = arrayOf(intArrayOf(1, 2, 3), intArrayOf(4, 5, 6), intArrayOf(7, 8, 9))
// 建立存放5个IntArray的数组, 每个IntArray的长度为3, 用0填充
val arr2 = Array(5) { IntArray(3){0} }
// 内层使用Any类型就可以接收所有类型的嵌套数组
val arr3: Array<Array<out Any>> = arrayOf(arrayOf(1, 2, 3), arrayOf("A", "B", "C"))

泛型

Kotlin的泛型和java相同,都通过<>表示。

类型上界

约束一个泛型类只接受一个类型的对象或者类及其子类,称为上界约束。

1
2
3
4
5
6
7
8
9
10
11
12
open class Fruit(val weight: Double)

class Apple(weight: Double): Fruit(weight)
class Banana(weight: Double): Fruit(weight)
class Noodles(weight: Double)
// 这里的T只能是Fruit类及其子类
class FruitPlate<T: Fruit>(val t: T)

// 允许
val applePlate = FruitPlate(Apple(100.0))
// 不允许
val noodlesPlate = FruitPlate(Noodles(100.0))

在Kotlin中,可以通过where关键字对泛型参数类型添加多个约束条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Ground {}

open class Fruit(weight:Double)

class Watermelon(weight:Double): Fruit(weight), Ground
class Apple(weight: Double): Fruit(weight)

fun <T> cut(fruit: T) where T: Fruit, T: Ground {
print("You can cut me.")
}

fun main() {
// 类型匹配
cut(Watermelon(10.0))
// 类型不匹配,不允许
cut(Apple(10.0))
}

类型擦除

泛型的类型检查仅仅只存在于编译阶段,在编译之后并不会保留任何关于泛型的内容。这便是类型擦除。类型擦除后一般为Any?,如果存在上界,那么擦除后是上界的类型。

1
2
3
4
5
6
class TypeClear<T>(private val t: T) {
fun isType(obj:Any):Boolean{
// 编译错误,由于类型擦除,运行时不存在T的类型
return obj is T
}
}

对于内联函数,泛型擦除的处理有一些不同。内联函数的泛型参数的具体类型信息是可用的,编译一起可以使用这些信息来生成具体的字节码。

为了避免频繁地类型转换,通常可以配合泛型封装成一个类型转换方法。我们还要避免类型擦除,可以用reified修饰T,并在方法前用inline修饰。**reified关键字可以理解为具体化,使我们可以在方法体内访问泛型指定的JVM对象。**

1
inline fun <reified T> cast(original: Any): T? = original as? T

协变和逆变

对于泛型来说,可以简单理解为:协变是可读不可写,逆变可写不可读。协变用out关键字表示,逆变用in关键字表示。

对于协变,如果类型A是类型B的子类型,那么Generic<A> 也是Generic<B>的子类型。

对于逆变,如果类型A是类型B的子类型,那么Generic<B> 将变为Generic<A>的子类型。

out关键字声明的泛型参数类型将不能作为方法的参数类型,但是可以作为方法的返回值类型,对应的setter也会被限制。in关键字与out相反,可以作为方法的参数类型,但是不能作为方法的返回值类型,对应的getter也会被限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Test<T>(var data:T)
open class A
class B:A()
class C:A()

fun main() {
val test1:Test<A> = Test(B())
// A被声明为out,setter方法被限制
val test2:Test<out A> = test1
test2.data = C()

val test3:Test<A> = Test(C())
val test4:Test<in C> = test3
// A被声明为in,getter方法被限制
val data:C = test3.data
}

可以通过*来代替任意类型,本质上相当于out Any?

1
2
3
4
class Test<T>(var data: T)

val test1: Test<String> = Test("Hello")
val test2: Test<*> = test1

Lambda和集合

任何函数接收了一个java的SAM(单一抽象方法)都可以用Kotlin函数进行替代。

1
2
3
4
5
6
view.setOnClickListener(new OnClickListener(){
@override
public void onClick(View v){
...
}
})

对于上述java代码,我们可以看成在Kotlin中定义了以下方法:

1
2
3
4
5
6
// listener是一个函数类型的参数,它接收一个类型为View的参数,返回Unit
fun setOnClickListener(listener: (View) -> Unit)
// 我们可以通过lambda表达式简化它
view.setOnClickListener({ ... })
// listener是唯一参数,可以简化掉()
view.setOnClickListener{ ... }

带接收者的Lambda

我们可以定义带有接收者的函数类型。

1
2
val sum:Int.(Int) -> Int = { other -> plus(other) }
2.sum(3) // 5

with和apply

Kotlin提供了withapply两个函数,省略了需要多次书写的对象名,默认用this指向它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun bindData(bean: ContentBean){
val titleTV = findViewById<TextView>(R.id.iv_title)
val contentTV = findViewById<TextView>(R.id.iv_content)
// with版本
with(bean){
titleTV.text = this.title
contentTV.text = this.content
}
// apply版本
bean.apply{
titleTV.text = this.title
contentTV.text = this.content
}
}

以下是withapply在Kotlin中的定义。

1
2
inline fun <T, R> with(receiver: T, block: T.() -> R): R
inline fun <T> T.apply(block: T.() -> Unit): T

with函数的第一个参数为接收者类型,然后通过第二个参数创建这个类型的block方法,因此接收者对象调用block方法时,可以直接使用this来代表这个对象。apply函数直接被声明为T类型的扩展方法,block参数是一个返回Unit类型的函数。因此,with的block可以返回自由的类型,然而二者在很多情况下都是可以互相替换的。

集合

Kotlin中常用的集合包括ListSetMap

List表示一个有序可重复的列表。

Set表示一个不可重复的集合。Set的实现方式分为HashSetTreeSet。HashSet通过Hash散列来存储元素,不能保证元素的有序性。TreeSet底层结构是二叉树,可以保证元素的有序性。

Map和其他集合不同,没有实现Iterable或者CollectionMap通过键值对来表示元素。

可变集合和有序集合

可变集合的创建方式带有mutable前缀,而只读集合一般为默认的创建方式。

1
2
3
4
5
6
7
// 可变集合
val listArr = mutableListOf(1,2,3,4,5)
// 只读集合
val setArr = setOf(1,2,3,4,5)

listArr[0] = 1 // 可变集合可以修改元素
// setArr[0] = 1 // 只读集合不能修改元素,会报错

迭代器

迭代器是每个集合类、数组都可以生成的东西,其作用是用于对内容的遍历。

1
2
3
4
5
val list = mutableListOf(1, 2, 3, 4, 5)
val iterator = list.iterator()
while (iterator.hasNext()) {
println(iterator.next())
}

对于List集合来说,Kotlin提供了一个特殊的迭代器listIterator

1
2
3
4
5
6
7
val list = mutableListOf(1, 2, 3, 4, 5)
val iterator = list.listIterator()
// 反向迭代
iterator.hasPrevious()
iterator.previous()
// 下一个元素的下标
iterator.nextIndex()

Kotlin还为Mutable集合提供了一个特殊的用于生成MutableIterator的函数,只要不是只读的集合类,都可以使用这个特殊的迭代器。其支持在遍历集合时对元素进行删除。

1
2
3
4
5
6
7
val list = mutableListOf(1, 2, 3, 4, 5)
val it = list.iterator()
while (it.hasNext()) {
println(it.next())
it.remove()
}
println(list) // [] 集合中元素已被删除,为空

惰性集合

在Kotlin中,可以通过sequence(序列)节省资源开销。在序列中,元素的求值是惰性的,只有在需要时才进行求值,而不是在它被绑定到变量后就立刻求值。(惰性求值)

1
2
3
4
5
6
7
8
9
10
11
val list = mutableListOf(1, 2, 3, 4, 5)
val list2 = list.asSequence()
.filter {
println("filter: $it")
it > 2
}
.map {
println("map: $it")
it * 2
}
println(list2) // kotlin.sequences.TransformingSequence@179d3b25

上述操作中,filtermap中的println并没有被执行。

中间操作和末端操作

在对普通集合进行链式操作的时候,有些操作会产生中间集合,当用这类操作对序列进行求值时,它们就被称为中间操作,例如上面的filtermap。这就是惰性求值的提现,也被称为延迟求值。然而,在对集合进行操作时,我们在意的大部分都是结果,而不是中间过程。末端操作就是返回结果的操作。在执行末端操作时,会触发中间操作的延迟计算。也就是满足了惰性操作的“被需要时”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
val list = mutableListOf(1, 2, 3, 4, 5)
val list2 = list.asSequence()
.filter {
println("filter: $it")
it > 2
}
.map {
println("map: $it")
it * 2
}
.toList()
println(list2)
// 结果
filter: 1
filter: 2
filter: 3
map: 3
filter: 4
map: 4
filter: 5
map: 5
[6, 8, 10]

在上述例子添加了末端操作toList后,所有的中间操作都被执行了。

无限序列

利用惰性求值的特性,可以通过序列构造出一个无限的数据类型。因为元素只有在被需要时才进行求值,所以无限序列只是实现了一种无限的状态,让我们在使用时感觉它是无限的。

1
2
3
4
5
// 将自然数存储到序列中: 0, 1, 2, ..., n
val natureNumbers = generateSequence(0) { it + 1 }
val list = natureNumbers.takeWhile { it < 10 }.toList()
println("list = $list")
// list = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

多态和扩展

多态

常见的多态分为子类型多态、参数多态和特设多态三种。

当我们用子类型去继承一个父类的时候,就是子类型多态。

参数多态

参数多态是指声明与定义函数、复合类型、变量时不指定其具体的类型,而是把这部分作为参数使用,使得该类型对各种具体类型都适用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface KeyI{ val uniqueKey: String }

class ClassA(override val uniqueKey: String): KeyI
class ClassB(override val uniqueKey: String): KeyI

fun <T: KeyI> persist(t: T){
println("Persisting ${t.uniqueKey}")
}

fun main() {
val a = ClassA("A1")
val b = ClassB("B1")
persist(a) // Persisting A1
persist(b) // Persisting B1
}

特设多态

对于特设多态,可以理解为一个多态函数是有多个不同的实现,依赖于其实参而调用相应版本的函数。例如函数重载、操作符重载和泛型。以上三种方式,编译器会根据传入参数的类型来选择最匹配的函数或操作符重载来执行,从而实现特设多态的效果。

扩展

在面向对象中已经介绍了类的扩展,在此将对扩展进行详细的说明。

在修改现有代码时,遵循开放封闭原则,对扩展开放,对修改封闭。

扩展函数

扩展函数的声明非常简单,其关键字是<Type>。此外还需要一个接收者类型作为它的前缀。在扩展函数体里,可以用this代表接收者类型的对象。

MutableList<Int>为例,我们扩展一个exchange方法,实现元素位置的交换。

1
2
3
4
5
6
7
8
9
10
11
12
fun MutableList<Int>.exchange(from: Int, to: Int){
val temp = this[from]
this[from] = this[to]
this[to] = temp
}

fun main() {
val list = mutableListOf(1, 2, 3, 4, 5)
// 交换下标为1和3的元素
list.exchange(1, 3)
println(list) // [1, 4, 3, 2, 5]
}

实现机制

我们可以将扩展函数理解为java中的静态方法,独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。因此扩展函数不会带来额外的性能消耗。

作用域

我们习惯将扩展函数直接定义在包内。

为了便于管理,可以将扩展函数定义在一个类内部。当扩展方法定义在一个类内部时,只能在该类和其子类中进行调用。

扩展属性

与扩展函数类似,可以为一个类添加扩展属性。

还是以MutableList<Int>为例,我们可以为其扩展一个判断其中元素之和是否为偶数的属性sumIsEven

1
2
3
4
5
val MutableList<Int>.sumIsEven: Boolean
get() = this.sum() % 2 == 0

val list = mutableListOf(1, 2, 3, 4, 5)
println(list.sumIsEven) // false

我们也可以定义类似java静态函数一样的扩展函数,但是必须将其定义在伴生对象上。

1
2
3
4
5
class Son{ companion object{ val age: Int = 18 } }

fun Son.Companion.foo() = println("$age")

Sun.foo() // 18

这样就可以在Son没有实例对象的情况下,也能调用这个扩展函数。语法类似于java的静态函数。

在使用扩展函数时,同名的类成员方法的优先级高于扩展函数。

1
2
3
4
class Fruit{ fun eat() = println("eat fruit") }
fun Fruit.eat() = println("eat apple")

fruit.eat() // eat fruit

元编程

描述数据的数据叫做元数据,操作元数据的编程就是元编程。通过元编程可以消除一些模板代码。

例如,我们需要将data class转化为Map

1
2
3
4
5
6
7
data class USer(val name: String, val age: Int)

object User{
fun toMap(a: User): Map<String,Any> {
return hashMapOf("name" to name, "age" to age)
}
}

如果在data class比较多的情况下,就会出现大量和上面类似的样板代码。我们可以通过反射解决这个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
object Mapper{
fun <T: Any> toMap(a: T): Map<String, Any?> {
return a::class.memberProperties.map {
it.name to it.call(a) }.toMap()
}
}
// 也可以直接写成扩展函数的形式
fun <T : Any> T.toMap(): Map<String, Any?> {
return this.javaClass.kotlin.memberProperties.associateBy(
{ it.name },
{ it.get(this) }
)
}

fun main() {
val person = Person("Alice", 25)
val map = Mapper.toMap(person)
// val map = person.toMap()
println(map)
}

通过使用反射,我们可以让上述代码适用于所有data class,而不需要针对每个去单独构造出一个toMap函数。也不需要手动创建Map里的属性名,可以根据KClass自动获取。

Kotlin中的反射

先来对比一下java和Kotlin中的反射。

Java反射

Kotlin反射

我们可以得出以下结论:

  1. KClassClass可以看做同一个含义的类型,并且可以通过.java.kotlin方法实现两者的相互转化。
  2. KCallableAccessiableObject都可以理解为可调用元素。java中的构造方法作为一个独立的类型,Kotlin则统一用KFunction处理。
  3. KProperty通常指相应的GetterSetter整体作为一个KProperty,java的Field通常仅仅指字段本身。

KClass

KClass出了和java的Class非常相似之外,还有独属于Kotlin的属性或方法。

属性或函数 含义
isCompanion 是否为伴生对象
isData 是否为数据类
isSealed 是否为密封类
objectInstance object实例(如果是)
companionObjectInstance 伴生对象实例
declaredMemberExtensionFunctions 扩展函数
declaredMemberExtesionProperties 扩展属性
memberExtensionFunctions 本类及超类扩展函数
memberExtensionProperties 本类及超类扩展属性
starProjectedType 泛型通配类型

下面通过一个例子理解上述方法或属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sealed class Cat {
companion object {
data object TOMCAT : Cat()
}

val Companion.getCat
get() = TOMCAT

fun <C : Cat> Animal<C>.proceed(): C = this.cat
}

data class Animal<C : Cat>(val cat: C) : Cat()

fun <C : Cat> Cat.plus(other: C): Cat {
return when {
other is Animal<*> -> Animal(other.cat)
else -> this
}
}

上述方法的调用结果如下表所示。

方法 结果
Cat.Companion::class.isCompanion true
Cat::class.isSealed true
Cat.Companion::class.objectInstance 包名.Cat$Companion@地址
Cat::class.declaredMemberExtensionProperties.map { it.name } [getCat]
Cat::class.declaredMemberExtensionFunctions.map { it.name } [proceed]
Animal::class.memberExtensionProperties.map { it.name } [getCat]
Animal::class.memberExtensionFunctions.map { it.name } [proceed]
Animal::class.starProjectedType 包名.Animal<*>

KCallable

Kotlin把Class中的属性Property和函数Function以及构造函数都看做KCallable,因为它们是可调用的,都是Class的成员。

KCallAble提供了一些实用的API。这些API和java中的反射的API很相似,都是对KCallable(Class成员)的信息的获取。

API 描述
isAbstract: Boolean<KParameter> KCallable是否为抽象的
isFinal: Boolean KCallable是否为final
isOpen: Boolean KCallable是否为open
name: String KCallable的名称
parameters: List<KParameter> 调用此KCallable所需的参数
returnType: KType KCallable的返回类型
typeParameters: List<KTypeParameter> KCallable的类型参数
visibility: KVisibility? KCallable的可见性
call(vararg args: Any?): R 给定函数调用此KCallable

在java中,我们可以通过Field.set(...)来更改某个属性的值。在Kotlin中,可以很轻松的通过call实现。

1
2
3
4
5
6
7
8
9
10
11
12
data class Stu(val name: String, val age: Int, var address: String)

fun changeAddress() {
val p = Stu("John", 25, "New York")
val props = p::class.memberProperties
for (prop in props) {
when (prop) {
is KMutableProperty<*> -> prop.setter.call(p, "London")
else -> prop.call(p)
}
}
}

KMutableProperty的API仅仅只比KProperty多了一个setter函数。

其他反射及其API自行查阅官网文档。

Kotlin的注解

在 Kotlin 中,注解(Annotations)具有以下作用:

  1. 提供元数据:注解可以用来提供关于程序元素的额外信息。通过在代码中添加注解,你可以指定程序元素的某些特定细节或属性,这些信息可以被其他代码或工具所利用。
  2. 用于静态检查和编译时处理:Kotlin 中的注解可以被用于在编译时检查代码,并触发特定的行为或处理。这样可以帮助你在编译时对代码进行额外的检查或生成额外的代码。
  3. 框架集成和元编程:注解在许多框架中起到重要作用,比如依赖注入、RESTful 服务的开发、ORM、序列化和反序列化等。注解还被广泛用于编写元编程的代码,比如自动生成代码或进行代码分析等。
  4. 用于标记和文档化:注解可以用于标记特定的代码块、类或函数,并且在文档生成工具中用于生成文档。通过使用注解,你可以方便地为代码添加标记和文档信息。

Kotlin还引入了精确的注解控制语法。

用法 含义
@file:annotation annotation注解作用于文件
@property:annotation annotation注解作用于属性
@field:annotation annotation注解作用于字段
@get:annotation annotation注解作用于Getter
@setter:annotation annotation注解作用于Setter
@receiver:annotation annotation注解作用于扩展函数或属性
@param:annotation annotation注解作用于构造函数参数
@setparam:annotation annotation注解作用于Setter的参数
@delegate:annotation annotation注解作用于存储代理实例的字段

在Kotlin中,通过在class前增加annotation关键字即可创建注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
annotation class Cache(val namespace: String, val expires: Int)
annotation class CacheKey(val keyName: String,val buckets: IntArray)

@Cache("hero", 3600)
data class Hero(
@property:CacheKey("name", [10, 20, 30])
val name: String,
@field:CacheKey("attack", [10, 20, 30])
val attack: Int,
@get:CacheKey("defense", [10, 20, 30])
val defense: Int,
val initHp: Int
)

协程

协程的启动

协程需要运行在协程上下文环境,在非协程环境中凭空启动协程,有三种方式。

  1. runBlocking{}

    启动一个新协程,并阻塞当前线程,直到其内部所有逻辑以及子协程逻辑全部执行完成。

  2. GlobalScope.launch{}

    在应用范围内启动一个新协程,协程的生命周期与应用程序一致。这样启动的协程并不能使线程保活,就像守护线程。

    由于这样启动的协程存在启动协程的组件已被销毁但协程还存在的情况,极限情况下可能导致资源耗尽,因此并不推荐这样启动,尤其是在客户端这种需要频繁创建销毁组件的场景。

  3. 实现CoroutineScope + launch{}

    这是在应用中最推荐使用的协程使用方式——为自己的组件实现CoroutineScope接口,在需要的地方使用launch{}方法启动协程。使得协程和该组件生命周期绑定,组件销毁时,协程一并销毁。从而实现安全可靠地协程调用。