Aengus 的技术小筑

Aengus | Blog

Kotlin 学习笔记——集合、协程

930
2020-07-03

集合Collections

Kotlin中的集合和Java中的非常像。

List,Set和Map

在Kotin中可以使用listOf<T>(),setOf<T>(),mapOf<K, V>()方法非常方便的生成List,Set和Map,但是需要注意的是上面提到的方法生成的集合以及Map都是不可变的,只能访问,不能操作,如果想生成可变的集合,可以使用下面的方法:

val mutableList = mutableListOf<Int>(1, 2, 3)  // [1, 2, 3]
val mutableSet = mutableSetOf<Int>(1, 2, 3, 4, 4, 5)  // [1, 2, 3, 4, 5]
val mutableMap = mutableMapOf<Int, String>(1 to "a", 2 to "b") // {1=a, 2=b}

在Kotlin中,List的默认实现是ArrayList,Set的默认实现是LinkedHashSet,Map的默认实现是LinkedHashMap,这些默认实现都可以保留元素插入的顺序,而HashSet、HashMap则不会。

在map的声明中,使用to为map指定键值对,这里的to是我们前面提到的中缀表达式,它的作用是将两个元素包装成一个Pair并返回,其声明如下:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

public data class Pair<out A, out B>(
    public val first: A,
    public val second: B
) : Serializable {
    public override fun toString(): String = "($first, $second)"
}

可以使用emptyList<T>(),emptySet<T>(),emptyMap<K, V>()创建空的集合。

使用in关键字可以对集合进行遍历与判断元素是否在集合内:

for (item in list) { /* ... */ }
if (2 in list) // 实际上调用的是list.contains(2)

Sequences

Kotlin标准库中还提供了另外一种集合叫做Sequences,这种集合相较于Iterable避免了中间步骤中对象的生成,也就是:对于同样的一系列操作,Iterable会对整个集合一步一步的进行操作,在每步操作后会生成对象,而Sequences会对集合中的每个元素进行所有的操作,所以不会有大的中间对象生成。使用下面的方法可以生成Sequences

val seq1 = sequenceOf("a", "b", "c")
// 也可以从其他对象中生成
val seq2 = listOf("a", "b", "c").asSequence()
// 还可以使用函数生成
val seq3 = generateSequence(1) {
    if (it < 10) it + 2 else null
}
seq3.forEach {
    println(it)
}
// 1 3 5 7 9 11

上面的generateSequence()例子中,表示从1开始,然后对其不断进行函数参数中的运算,直到Lambda返回null停止生成。

运算符重载

Kotlin为集合类重写了常用操作符,如下:

val a = list[0]    // => list.get(0)
val b = map["key"] // => map.get("key")
map["key"] = 10    // => map.set("key") = 10
 // c.addAll(listOf(1, 2, 3)) c.addAll(listOf(2, 4, 5))
val c = listOf(1, 2, 3) + listOf(2, 4, 5) // c = [1, 2, 3, 2, 4, 5]

// c.addAll(listOf(1, 2, 3)) c.removeAll(listOf(2, 4, 5))
val c = listOf(1, 2, 3) - listOf(2, 4, 5) // c = [1, 3]

协程Coroutines

并发是指多个线程在系统的调度下通过时间片轮转的方式占有系统资源进行运算,当系统进行线程切换时,需要保存当前线程的上下文信息,然后再进行切换,这是一个比较消耗时间的操作,协程便是为此而生的。协程常常被称为轻量级线程,它可以提供线程的功能而且没有线程切换那么重。而我们将要讨论的是安卓上的协程,由于安卓基于JVM运行,所以Kotlin上的协程仍然需要在JVM上实现,而在JVM上,协程最终的实现方式还是线程,所以就安卓来说,Kotlin的协程实际上是一款线程框架,它仅仅是封装了线程的API,方便我们使用,Kotlin上的协程不是广义上的协程

简单的例子如下:

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch {
        delay(1000L)
        print("World!")
    }
    print("Hello, ")
    Thread.sleep(2000L)
}
// 输出:
// Hello, World!

这个例子中,首先使用GlobalScope.launch()启动了一个协程,然后让其delay()了1秒,这时主线程继续运行,打印出Hello,,然后主线程sleep()两秒,这时候协程delay时间已到,继续执行打印出World!GobalScopeCoroutineScope的一种,它代表这个协程的生命周期和整个应用一样长。

可以使用Thread().start()Thread.sleep()实现上面的效果:

fun main() {
    // thread {} 是Kotlin提供的函数,可以不调用 start() 直接使用
    thread {
        Thread.sleep(1000L)
        print("World!")
    }
    print("Hello, ")
    Thread.sleep(2000L)
}

但是如果将thread {}块中的Thread.sleep()换成delay()就会报错,这是因为delay()是挂起函数。

挂起函数

使用suspend关键字修饰的函数是挂起函数,挂起函数只能在协程中被调用或者在另一个挂起函数中调用(而挂起函数中可以调用普通函数),suspend关键字在语法层面上的作用是标记和提醒suspend函数可以提醒使用者“我是一个耗时任务,你需要在一个协程中调用我”;在编译器层面,suspend关键字辅助 Kotlin 编译器来把代码转换成 JVM 的字节码:

suspend fun <R> run(suspend (R) -> Unit): R

为什么用协程

在协程的介绍中我们发现用thread可以很方便的实现协程的功能,而且还更简洁,那么为什么要用协程呢,我们首先看一段代码:

如果我们有几个UI线程上的任务和IO线程上的任务需要轮换运行,如果使用线程的方式写的话:

fun uiThreadTask1()
fun ioThreadTask1()
fun uiThreadTask2()
fun uiThreadTask2()
fun main() {
    uiThreadTask1()
    thread {
        ioThreadTask1()
        runOnUIThread {
            uiThreadTask2()
            thread {
                ioThreadTask2()
            }
        }
    }
}

可以看到用普通线程的方法实现的线程切换的嵌套十分严重,在实际业务中情况更复杂,那么代码可读性也越差。用协程的方式可以用同步的方式写出异步的代码:

fun main() {
    GlobalScope.launch(Dispatchers.Main) {
        uiThreadTask1()
        withContext(Dispatchers.IO) {
            ioThreadTask1()
        }
        uiThreadTask2()
        withContext(Dispatchers.IO) {
            ioThreadTask2()
        }
    }
}

首先,我们给launch()函数传入了一个参数Dispatchers.Main,代表接下来的函数都在主线程中进行;接着我们直接执行UI线程的代码;接下来的IO线程的代码我们放在withContext()函数中,并给其传入参数Dispatchers.IOwithContext()函数可以帮我们自动切换到参数指定的线程,并在代码直接结束后切换回来,所以待IO线程代码执行完成后,协程帮我们自动切换回来主线程,再接着执行uiThreadTask2()

如果我们直接将withContext()函数放在需要切换线程的代码函数中,还可以更好的增加代码的可读性,如下:

// 由于 withContext() 函数是挂起函数,所以我们的函数也要声明为挂起函数
suspend fun ioThreadTask1() {
    withContext(Dispatcher.IO) {
        // ...
    }
}
suspend fun ioThreadTask2() {
    withContext(Dispatchers.IO) {
        // ...
    }
}
fun main() {
    GlobbalScope.launch(Dispatchers.Main) {
        uiThreadTask1()
        ioThreadTask1()
        uiThreadTask2()
        ioThreadTask2()
    }
}

上面的代码就是用同步的方式调用异步的代码,代码可读性也非常好。

launch()函数和withContext()函数的声明如下:

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

我们为其传入的参数Dispatchers有如下几个值:

// 代表在JVM共享的线程池中执行
// 这个调度器可用的最大并行数量和CPU核数相等,最少是2
Dispatchers.Default
// 操作UI对象的主线程,平台相关
// Android上是Main线程
// JavaFX上是JavaFx Application线程
// Swing上是Swing EDT调度器
Dispatchers.Main
// 它与Dispatcher.Default共享线程池,如果之前的线程就是Default,那么可能不会切换线程
// 它将堵塞的IO操作分发到共享线程池中
Dispatchers.IO

注意到launch()函数拥有一个返回值Job,所以我们可以用join()函数堵塞线程等这个协程完全执行完成:

suspend fun main() {
    val job = GlobalScope.launch {
        delay(2000L)
        println("Processing")
    }
    println("Start...")
    job.join()	// join()是挂起函数
    println("End...")
}
// 输出:
// Start...
// Processing
// End...