Kotlin笔记

Kotlin 与 Java 的关系

Java运行过程:Java源代码 -> 编译 -> 生成class文件 -> JVM解释执行

Java虚拟机并不关心class文件是如何生成的,因此也可以使用Kotlin生成class文件,JVM也可以解释执行。

Kotlin可以无缝使用Java第三方的开源库。

Q:Android为什么推荐Kotlin?Kotlin与Java比有哪些优势?

A:

  1. 空安全
  2. Lambda表达式和高阶函数
  3. Kotlin更简洁,引入了数据类、get、set方法等
  4. 拓展函数
  5. 支持协程
  6. 完全兼容Java

val 与 var

类比JavaScript中的constlet

val用于声明常量,相当于加了final,其引用不可变,赋值后无法修改;var用于声明变量。

空安全

Q:哪些情况会出现NPE(NullPointException)?

A:

  • 强制!!
  • lateinit但未初始化
  • 与Java通信,Java返回的空值
  • 泛型擦除导致

Any、Unit

  • Any等同于Java中的Object,是所有类的父类
  • Unit等同于void,当函数无返回值时会自动加上返回值Unit

if

Kotlin中的if是带有返回值的,返回值即花括号内的最后一行表达式的值:

1
2
3
4
5
val max = if(a > b) {
a
} else {
b
}

字符串

字符串模板

$后花括号内可以是一个表达式:

1
2
val str = "你好"
println("$str${str.replace("你好", "再见")}!") // 你好,再见!

多行字符串

三对双引号内的字符串可换行:

1
2
3
4
5
6
7
8
val poem = """
关关雎鸠,
在河之洲。
窈窕淑女,
君子好逑。
""".trimIndent()

println(poem)

其中trimIndent函数的作用是去除输入行的公共最小缩进(空行不影响)。输出为

1
2
3
4
关关雎鸠,
在河之洲。
窈窕淑女,
君子好逑。

有时为了让代码更清晰,我们可以使用|符号结合trimMargin函数来标记实际字符串的开始位置:

1
2
3
4
5
6
7
8
val poem = """
| 蒹葭苍苍,
|白露为霜。
| 所谓伊人,
|在水一方。
""".trimMargin()

println(poem)

其中trimMargin函数的作用是将多行字符串中的每行内容使用特定的前导字符(默认为|)进行标记,然后去除这些字符及其前面的空白。输出为

1
2
3
4
  蒹葭苍苍,
白露为霜。
所谓伊人,
在水一方。

也可指定不同的前导字符:

1
2
3
4
5
6
7
8
val poem = """
>郎骑竹马来,
> 绕床弄青梅。
>同居长干里,
> 两小无嫌猜。
""".trimMargin(">")

println(poem)

when

相当于switch,不过用起来更方便。

1
2
3
4
5
6
7
fun getScore(name: String) = when(name) {
"张三" -> 85
"李四" -> {
return 91
}
else -> 0
}

区间

闭区间

1
val range = 0..10   // [0, 10]

左闭右开区间

1
val range = 0 until 10   // [0, 10)

降序闭区间

1
val range = 10 downTo 0   // [10, 0]

循环

for - in 循环

1
2
3
4
5
6
7
8
9
10
// 输出 0~9
for (i in 0 until 10) {
println(i)
}

// 每次循环 i+=2
for (i in 0..10 step 2) {
println(i)
}
// 输出 0~10 内的偶数

构造函数

Kotlin中有主次构造函数之分。

主构造函数没有函数体,直接定义在类名的后面。若想在主构造函数中编写一些逻辑,将代码写在init中:

1
2
3
4
5
6
7
class Student(val name, val grade) {
init {
// 主构造函数的逻辑写在这里
}
}

Student('张三', 86)

次构造函数有函数体,且必须调用主构造函数。

1
2
3
4
5
6
7
8
9
10
11
class Student(val name, val grade) {
init {
// 主构造函数的逻辑写在这里
}

constructor(name: String) : this(name, 0) {
// 次构造函数逻辑
}
}

Student('张三', 86)

继承

Kotlin中非抽象类默认都是不可以被继承的,相当于给类加上了final。要声明为可继承类,需要加上open关键字。

同时子类中的构造函数必须调用父类中的构造函数。

1
2
3
4
5
6
7
8
9
10
11
open class Person(val name) {
//TODO
}

class Student(val name, val grade) : Person(name) {
init {
// 主构造函数的逻辑写在这里
}
}

Student('张三', 86)

接口

1
2
3
4
5
6
7
8
9
10
interface Rectangle {
fun getPerimeter()
fun getArea()
}

// Square类继承Shape类,同时实现Rectangle接口
class Square(edge: Double) : Shape(), Rectangle {
override fun getPerimeter() = edge * 4
override fun getArea() = edge * edge
}

单例

Java中实现单例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Singleton {
private static Singleton instance;

private Singleton() {}

public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}

public void singletonTest() {
System.out.println("singletonTest方法被调用");
}
}

Singleton singleton = Singleton.getInstance();
singleton.singletonTest();

Kotlin中实现单例十分简便,只需要将关键字class改为object即可:

1
2
3
4
5
6
7
object Singleton {
fun singletonTest() {
println("singletonTest is called.")
}
}

Singleton.singletonTest()

拓展函数

我们可以为某一个数据类型添加自定义的方法。

举个例子,如对时间进行格式化,一种普通做法是写一个工具类,用伴生对象实现类似静态方法的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class DateTimeUtil {
companion object {
private const val DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"

// 格式化时间
fun format(dateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)
return formatter.format(dateTime)
}
}
}

fun main() {
val dateTime = LocalDateTime.now()
println("$dateTime") // 2024-03-24T23:07:39

val formattedDateTime = DateTimeUtil.format(dateTime)
println("$formattedDateTime") // 2024-03-24 23:07:39
}

除此之外还有另一种更为优雅的方法:

1
2
3
4
5
6
7
8
9
10
11
fun LocalDateTime.format(): String {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
return formatter.format(this)
}

fun main() {
val dateTime = LocalDateTime.now()
println("$dateTime") // 2024-03-24T23:07:39

println(dateTime.format()) // 2024-03-24 23:07:39
}

这里调用的format函数是我们自定义的。

kotlin与dart都有这种特性。

Kotlin的拓展函数只是语法糖,本质是静态解析,编译期根据声明类型决定调用哪个函数。

可见性

可见性

List、Set、Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// List
val list = listOf("春秋左氏传", "春秋公羊传", "春秋谷梁传") // 不可变列表,不可添加元素
val mutableList = mutableListOf("诗经", "尚书", "礼记", "易经") // 可变列表
mutableList.add("春秋")

// Set
val set = setOf("白起", "王翦", "廉颇", "李牧")
val mutableSet = mutableSetOf("齐桓公", "晋文公", "秦穆公", "宋襄公")
mutableSet.add("楚庄王")

// Map
val map = mapOf("儒" to "孔子", "法" to "韩非子", "墨" to "墨子", "道" to "庄子")
val mutableMap = mutableMapOf("齐" to "临淄", "楚" to "郢都", "燕" to "蓟城", "韩" to "新郑", "赵" to "邯郸", "魏" to "大梁")
mutableList.add("秦" to "咸阳")

Lambda表达式

本质是匿名函数,即可当做值来传递的一段代码。

完整结构:val lambda = { param1: Type, param2: Type -> 函数体 },最后一行表达式的值作为整个Lambda表达式的返回值。

1
2
3
4
5
6
7
8
9
10
button.setOnClickListener({ data -> println(data) })

// 若Lambda表达式是函数的最后一个参数,可以把Lambda表达式提到括号外:
button.setOnClickListener() { data -> println(data) }

// 若Lambda表达式是唯一的参数可以省略括号:
button.setOnClickListener { data -> println(data) }

// 若Lambda表达式内只有一个参数,可以用`it`代指:
button.setOnClickListener { println(it) }

inline & noinline & crossinline(TODO)

Lambda的实现有两种情况:普通的非inline和inline。非inline的Lambda,本质是生成了一个匿名的函数对象,包含了Lambda中的方法,因此有创建对象的性能开销。inline则是编译期间直接展开代码。

Q:与Java中的Lambda区别?

A:

Java的Lambda本质是实现接口,而Kotlin的Lambda是一个函数对象。

另外Kotlin的Lambda可以非局部返回,如:

1
2
3
4
5
fun test() {
listOf(1, 2, 3).forEach {
return // 直接返回 test()
}
}

Java则不行。

数据类

数据类的copy方法是浅拷贝,只是复制引用,没有递归复制对象,深拷贝需要自行实现。

协程

Kotlin Jetpack 实战 | 09. 图解协程原理

别再死记硬背了!用‘红绿灯’和‘存档读档’理解Kotlin协程的挂起与恢复

协程是用户态的调度单元,不由操作系统调度,可以在一个线程上运行多个协程。线程运行在内核态,协程运行在用户态。

suspend函数可以挂起,挂起不是阻塞,线程不会停。挂起就是把函数拆成多个状态,通过Continuation串起来执行。

编译后的suspend函数内部基于状态机实现,Kotlin协程在编译期会把suspend函数转换为一个状态机。在launch {...}协程启动时会创建一个Continuation对象,用于保存恢复执行所需的所有上下文,最终触发invokeSuspend进入状态机。通过一个label变量记录状态机的分支状态,随后根据label状态决定执行when的哪个分支。例如刚开始label = 0进入第一个分支,则令label = 1,执行到耗时操作如IO时,协程用withContext(Dispatchers.IO) {...}Dispatchers.IO是一个线程池调度器)将IO操作调度到IO线程池中的某个线程执行。同时当前挂起函数返回CoroutineSingletons.COROUTINE_SUSPENDED,表示协程挂起,让线程继续执行其他任务。当异步任务完成后,再通过Continuation.resumeWith(Result)触发挂起函数,将之前的continuation传入复用恢复上下文,再根据label跳转到对应分支执行后续的代码。

协程的整个过程本质是CPS(Continuation Passing Style)转换,将同步代码拆分成多个状态执行。

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
viewModelScope.launch {
logUserFrinedList()
}

suspend fun logUserFrinedList() {
log("start")
val user = getUser()
log(user)
val friends = getFriends(user)
log(friends)
}

// 请求用户信息
suspend fun getUser(): User {
return suspendCancellableCoroutine { cont ->
api.getUser().enqueue(object : Callback<User> {
override fun onResponse(call: Call<User>, response: Response<User>) {
cont.resume(response.body()!!)
}
override fun onFailure(call: Call<User>, t: Throwable) {
cont.resumeWithException(t)
}
})
}
}

// 请求好友列表
suspend fun getUser(user: User)...

上面的logUserFrinedList()方法进行编译后,变为:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
fun logUserFrinedList(completion: Continuation<Any?>): Any? {

// 这里定义一些变量
// 对应原函数的两个变量
lateinit var user: User
lateinit var friends

// result 接收协程的运行结果
var result = continuation.result

// suspendReturn 接收挂起函数的返回值
var suspendReturn: Any? = null

// CoroutineSingletons 是个枚举类
// COROUTINE_SUSPENDED 代表当前函数被挂起了
val sFlag = CoroutineSingletons.COROUTINE_SUSPENDED


// 从这里开始看
val continuation = if (completion is LogUserFrinedList) {
// 不是初次运行,直接用之前的continuation,只产生一个实例节省开销
completion
} else {
// 1. 第一次触发 logUserFrinedList 方法,创建并赋值 continuation
LogUserFrinedList(completion)
}

// 2. 实例化该类作为 continuation
// 该内部类的实例用于存储协程状态与上下文变量
class LogUserFrinedList(completion: Continuation<Any?>?) : ContinuationImpl(completion) {
// 表示协程状态机当前的状态
var label: Int = 0

// 协程返回结果
var result: Any? = null

// 用于保存之前协程的计算结果
var mUser: Any? = null
var mFriends: Any? = null

// 5. 唤醒,最终调用 logUserFrinedList(this) 开启协程状态机
// 8.
override fun invokeSuspend(_result: Result<Any?>): Any? {
result = _result
label = label or Int.Companion.MIN_VALUE
return logUserFrinedList(this)
}
}

when (continuation.label) {
0 -> {
// 3. 第一个挂起点前
// 检测异常
throwOnFailure(result)

log("start")
// 将 label 置为 1,准备进入下一次状态
continuation.label = 1

suspendReturn = getUser(continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
// 4. 挂起,等待 getUser
return suspendReturn
} else {
// 若无需挂起则直接到下一个状态
result = suspendReturn
// go to next state
}
}

1 -> {
// 6. 第一个挂起点与第二个挂起点之间
throwOnFailure(result)

user = result as User
log(user)
// 将协程结果存到 continuation 里
continuation.mUser = user
continuation.label = 2

// 执行 getFriends
suspendReturn = getFriends(user, continuation)

// 判断是否挂起
if (suspendReturn == sFlag) {
// 7. 挂起,等待 getFriends
return suspendReturn
} else {
result = suspendReturn
// go to next state
}
}

2 -> {
// 9. 第二个挂起点后
throwOnFailure(result)

user = continuation.mUser as User
friends = continuation.mFriends
loop = false
}
}

return Unit
}

Q:为什么suspend必须在协程中使用?

A:

suspend函数依赖Continuation,而这个对象是由协程提供的,因此suspend函数必须在协程中调用。

Q:为什么采用Continuation而不是回调?

A:

如果用传统的回调方式,每个异步操作都需要创建新的回调对象,且需要手动保存需要的局部变量,参数需要一层层带着。而用Continuation则整个协程只创建一个Continuation对象,编译器会自动提升局部变量到Continuation对象,比用回调方式更加高效便利。

Q:线程切换是怎么实现的?

A:

withContext会挂起当前协程,并在新的Dispatcher上恢复执行,切线程本质是恢复发生在另一个Dispatcher上。

Q:Thread.sleep()delay()的区别

A:

Thread.sleep()是让线程停住,而delay()是挂起,不是阻塞,线程可以干别的,只是这段代码暂停。

协程的取消

TODO

协程的生命周期 & Job

TODO

协程中的线程安全

Q:为什么协程可以安全更新UI?

A:

Dispatchers.Main保证在主线程执行。