kotlin从Java到入门

主要是基于大二学生的java水平,一些java中的(或者很相似的)基本都省略了,一些冷门的也是。

现在这里仅仅是我对官网教程的粗摘以及很少的笔记,后续如果继续学习会进一步增补。

谁再说kt像java我一定给他一jio

不过该有的都有

可以在 Kotlin 中调用 Android 或其他 Java 语言库的 API 。Kotlin 与 Java 语言具有互操作性。此设计让 Kotlin 代码可以透明地调用 Java 语言方法;对于 Kotlin 独有的功能,可采用注释轻松向 Java 代码公开。 对于未使用任何 Kotlin 特有语义的 Kotlin 文件,Java 代码可以直接引用,无需添加任何注释。两相结合,就可以同时使用 Java 代码和 Kotlin 代码。

可以在同一个项目中同时使用 Java 文件和 Kotlin 文件。可以根据自己的喜好或多或少采用 Kotlin 语言进行开发。java和kotlin可以同时存在,可以混编开发。

Tips:

我的IDEA有点小问题,我不确定是否只有我会出现这种情况。

我的kt代码的main()都无法直接运行(新建项目时语言选的是kt),但是可以通过java的main()去调用kt类中的main(),于是我就使用这种方法运行kt代码。

后来找到了原因,kt不要把main()放在某个类里,这种风格更偏向py而非java。

 

数据类型

一些数据类型

整数类型

类型位宽最小值最大值
Byte8-128127
Short16-3276832767
Int32-2,147,483,648 (-2^31)2,147,483,647 (2^31 - 1)
Long64-9,223,372,036,854,775,808 (-2^63)9,223,372,036,854,775,807 (2^63 - 1)
1
2
3
4
val number =          100 //默认是 Int 类型
val bigNumber = 8000000000 //超过 Int 最大值默认是 Long 类型
val longNumber = 20L //数字后面显式加L,表示声明 Long 类型
val byteNumber:Byte = 1

Tips

所有未超出 Int 最大值的整型值初始化的变量都默认为Int类型,如果初始值超过了其最大值,那么推断为Long类型。在数字值后面显式添加L表示一个Long类型

 

Float、Double浮点类型

跟java一样,可略过

Kotlin 中提供了 FloatDouble 两种类型来分别表示单精度和双精度的浮点数类型。

类型位宽
Float32
Double64
1
2
val doubleNumber = 3.1415928888  //默认是Double类型
val floatNumber = 3.1415928888f //尾部加f或F显式表示这是一个Float类型的浮点数

Tips

Kotlin 对于小数的默认推断是Double类型。如果需要显式将一个小数指定为Float类型需要在数值尾部加入fF。由于Float类型十进制位数是6位,所以上述例子中floatNumber实际值大小为3.1415926,后面就会出现进度丢失舍弃。

在 Kotlin 中还有一点与 Java 不同的是,Kotlin 中数字不存在隐式的拓宽转换。比如一个函数参数为Double 的函数只能接收 Double 类型,不能接收 FloatInt 或者其他数字类型

 

布尔类型

跟java基本一样,可略过

在 Kotlin 使用Boolean表示布尔类型,它只有两个值 truefalse。注意可空类型Boolean?类型会存在装箱操作

1
2
val isVisible: Boolean = false
val isVisible = false //自动推断为布尔Boolean类型

 

字符类型

跟java一样,可略过

在 Kotlin 中字符用 Char 类型表示

1
2
3
fun testChar(char: Char) {
if(char == 4) {...}//此处会提示类型不匹配的异常
}

字符的值需要用单引号括起来: '0''9'

1
2
3
4
5
fun decimalDigitValue(c: Char): Int {
if (c !in '0'..'9')//这个..后面会讲,其实就是'0'~'9'
throw IllegalArgumentException("Out of range")
return c.toInt() - '0'.toInt() // 显式转换为数字,后面也会讲
}

 

字符串类型

这个。。。有点博采众长的感觉,建议看一下

在 Kotlin 中字符串用 String 类型表示。字符串是不可变的。 字符串的元素——字符可以使用索引运算符访问: s[i]。 可以用 for 循环迭代字符串(这与cpp类似):

1
2
3
4
5
val str="1234567890"

for(char in str) {
println(char)
}
字符串模板

字符串字面值可以包含模板表达式 ,即一些小段代码,会求值并把结果合并到字符串中。 模板表达式以美元符($)开头,由一个简单的名字构成:

1
2
val number = 100
println("The Result is $number")

或者用花括号${}括起来的任意表达式:

1
2
val text = "This is Text"
println("Text length is ${text.length}")

字符串与转义字符串内部都支持模板。 如果你需要在原始字符串中表示字面值 $ 字符(它不支持反斜杠转义),你可以用下列语法:

1
val price = "${'$'}9.99"

和 Java 一样,Kotlin 可以用 + 操作符连接字符串。这也适用于连接字符串与其他类型的值。

1
2
3
val age = 28
println("I am " + age + "years old!")
println("I am $age years old!")
字符串的值

Kotlin 有两种类型的字符串字面值:转义字符串可以有转义字符, 以及原始字符串可以包含换行以及任意文本。以下是转义字符串的一个示例:

1
2
val s = "Hello, world!\n"     // \n换行
val s2= "{\"key\":\"value\"}" // \反斜杠对""进行转义,保留字符串格式

字符串使用三个引号(""")分界符括起来,内部没有转义并且可以包含换行以及任何其他字符:

1
2
3
4
val text = """
for (c in "foo")
print(c)
"""

还可以通过 trimMargin() 函数去除前导空格:

1
2
3
4
5
6
val text = """
|Tell me and I forget.
|Teach me and I remember.
|{"key1": "value1"}
|{"key2": "value2"}
""".trimMargin()

 

类型强制转换

在 Kotlin 中与 Java 不同是通过调用 toInt、toDouble、toFloat 之类函数来实现数字类型的强制转换的。

相当于java数据类型的parse()

类型强转函数
BytetoByte()
ShorttoShort()
InttoInt()
LongtoLong()
FloattoFloat()
DoubletoDouble()
ChartoChar()
1
2
3
4
5
6
7
8
val number =100   //声明一个整形 number对象
number.toString()
number.toByte()
number.toShort()
number.toLong()
number.toFloat()
number.toDouble()
....

数字运算

四则运算

与Java同

位运算

Kotlin 中的位运算和 Java 不同的是没有用特殊符号来表示,可以采用了中缀函数方式调用具名函数。

  • shl(bits) – 有符号左移【shl是Shift Logical Left的缩写】
  • shr(bits) – 有符号右移
  • ushr(bits) – 无符号右移
  • and(bits) – 位
  • or(bits) – 位
  • inv() – 位
  • xor(bits) – 位异或

后面这四个可能更常用,以及这里与java还是有区别

1
2
3
4
val vip = true
val admin = false
val result = vip and(admin) =false
val result = 8 ushr(2) = 2

 

数据容器

数组

初始化时必须指定大小,不能动态调整大小,元素可重复。

创建

创建方式与java相比差别还是比较大

1
2
3
4
val array : Array<Int> = arrayOf(1,2,3) // 若显示指明为Array则必须加泛型
val array2 = arrayOf(1,"2",true)//这其实是 Array<{Comparable<*> & java.io.Serializable}>类型
val arrayOfNulls = arrayOfNulls<String>(4)//必须显示指明泛型与大小
val asc = Array(5){ i -> (i * i)}//相当于:Array<Int>(5){i -> (i * i)},只不过这里的泛型可以推导出来

原生类型数组

在Kotlin中也有无装箱开销的专门的类来表示原生类型数组:

毕竟java里的List泛型也必须装箱嘛对吧,这里就是避免装箱

原生类型数组解释
ByteArray字节型数组
ShortArray短整型数组
IntArray整型数组
LongArray长整型数组
BooleanArray布尔型数组
CharArray字符型数组
FloatArray浮点型数组
DoubleArray双精度浮点型数组
1
2
3
4
5
6
7
8
9
10
11
// 1.创建并初始化一个IntArray  [1, 2, 3, 4, 5]
val intArray = IntArray(1, 2, 3, 4, 5)

// 2.创建一个长度为5的空的IntArray
val intArray2 = IntArray(5)

// 3.创建一个长度为5的值全为100的IntArray [100, 100, 100, 100, 100]
val intArr2 = IntArray(5) { 100 }

// 4.注意这里it是它索引下标值,所以这是创建一个长度为5的IntArray [0, 2, 4, 6, 8]
val intArr3 = IntArray(5) { it * 2 }

Tips

Kotlin数组类型不是集合中的一种,但是它又和集合有着太多相似的地方。

数组和集合可以互换

初始化集合的时候可以传入一个数组

基本操作

for循环——元素遍历
1
2
3
for (item in array) { // 元素遍历
println(item)
}
for循环——下标遍历
1
2
3
for (i in array.indices) {  // 根据下标再取出对应位置的元素
println(i.toString() + "->" + array[i])=
}
for循环——遍历元素(带索引)
1
2
3
for ((index, item) in array.withIndex()) {  // 同时遍历下标 和 元素
println("$index->$item")
}
forEach遍历数组
1
2
3
array.forEach {
println(it)
}
forEach增强版
1
2
3
array.forEachIndexed { index, item ->
println("$index$item")
}

 

集合

集合元素数量可变,元素可能可以重复。

  • List: 是一个有序集合,可通过索引(反映元素位置的整数)访问元素。元素可以重复
    • 与数组的区别是长度可变
  • Set: 是唯一元素的集合。它反映了集合(set)的数学抽象:一组无重复的对象。一般来说 set 中元素的顺序并不重要。例如,字母表是字母的集合(set)。
  • Map: (或者字典)是一组键值对。键是唯一的,每个键都刚好映射到一个值,值可以重复(同Java)。
创建方式示例说明是否可变
arrayListOf() mutableListOf相同元素类型的队列val array = arrayListOf(1, 2, 3) val array = mutableListOf()必须指定元素类型可变
listOf() 相同元素类型的集合val array = listOf(1, 2, 3)必须指定元素类型,必须指定初始化数据元素不可变
arrayMapOf<K,V>() mutableMapOf<K,V> 相同元素类型的字典val array= arrayMapOf(Pair(“key”,“value”)) val array= mutableMapOf()初始元素使用Pair包装可变
mapOf() 相同元素类型的字典val array= mapOf(Pair(“key”,“value”))元素使用Pair包装,必须指定初始元素不可变
arraySetOf() mutableSetOf相同元素类型的集合val array= arraySetOf(1,2,3) val array= mutableSetOf()会对元素自动去重可变
setOf() 相同元素类型的集合val array= arraySetOf(1,2,3)对元素自动去重,必须指定元素类型。不可变

不难发现,每个不可变集合都有对应的可变集合,也就是以mutable或者array为前缀的集合。

增删改查

1
2
3
4
5
6
7
8
9
10
11
val stringList = listOf("one", "two", "one")  以list集合为例,set,map同样具备以下能力

numbers.add(5) // 集合最后面添加元素5
numbers.addAt(1,5) // 向下标为1的位置,添加元素5,下标为1及之后位置的元素,以此后移

numbers.remove("one") // 删除元素“one”
numbers.removeAt(1) // 删除下标为1的元素

numbers.set(0) = 0 // 下标为0的元素设置为0

numbers.get(0)==>1 // 获取下标为0的元素,结果为1

变换操作

在Kotlin中提供了强大对的集合排序的API,让我们一起来学习一下:

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
val numbers = mutableListOf(1, 2, 3, 4)
numbers.reverse() //列表翻转
>> println(numbers)= 4, 3, 2, 1

numbers.shuffle() //随机排列元素
>> println(numbers)= 1, 3, 4, 2

numbers.sort() //排序,从小打到
>> println(numbers)= 1, 2, 3, 4

numbers.sortDescending() //从大到小
>> println(numbers)= 4, 3, 2, 1

//定义一个Person类,有name 和 age 两属性
class Language{
var name: String='name',
var score: Int=100
}

val languageList: MutableList<Language> = mutableListOf()
languageList.add(Language("Java", 80))
languageList.add(Language("Kotlin", 90))
languageList.add(Language("Dart", 99))
languageList.add(Language("C", 80))

//使用sortBy进行排序,适合单条件排序
languageList.sortBy { it.score } //it变量是lambda中的隐式参数
>> println(languageList)= [{Java", 80},{"C", 80},{"Kotlin", 90},{"Dart", 99}]

//使用sortWith进行排序,适合多条件排序
languageList.sortWith(compareBy({ it.score }, { it.name }))
>> println(languageList)= [{"C", 80},{Java", 80},{"Kotlin", 90},{"Dart", 99}]

 

流程控制

此处差别较大,建议细看

Break 与 Continue 标签

在 Kotlin 中任何表达式都可以用标签来标记。 标签的格式为标识符后跟 @ 符号,例如:abc@fooBar@。 要为一个表达式加标签,我们只要在其前加标签即可。

1
2
3
loop@ for (i in 1..100) {
// ……
}

现在,我们可以用标签限定 break 或者 continue

1
2
3
4
5
loop@ for (i in 1..100) {
for (j in 1..100) {
if (……) break@loop
}
}

标签限定的 break 跳转到刚好位于该标签指定的循环后面的执行点。 continue 继续标签指定的循环的下一次迭代。

 

for-in遍历

1
2
3
4
5
6
7
for(i in 1 until 10 step 2){
print(i)
}
//等价于
for(i = 1; i < 10; i+=2){
print(i)
}
1
2
3
4
5
6
7
for(i in 10 downTo 1){
print(i)
}
//等价于
for(i = 10; i >= 1; i++){
print(i)
}

 

返回到标签

Kotlin 中函数可以使用函数字面量、局部函数与对象表达式实现嵌套。 标签限定的 return 允许我们从外层函数返回。 最重要的一个用途就是从 lambda 表达式中返回。回想一下我们这么写的时候, 这个 return 表达式从最直接包围它的函数——foo 中返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return // 非局部直接返回到 foo() 的调用者
print(it)
}
println("this point is unreachable")
}
//sampleEnd

fun main() {
foo()
}

注意,这种非局部的返回只支持传给内联函数的 lambda 表达式。 如需从 lambda 表达式中返回,可给它加标签并用以限定 return

1
2
3
4
5
6
7
8
9
10
11
12
13
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者——forEach 循环
print(it)
}
print(" done with explicit label")
}
//sampleEnd

fun main() {
foo()
}

现在,它只会从 lambda 表达式中返回。通常情况下使用隐式标签更方便,因为该标签与接受该 lambda 的函数同名。

1
2
3
4
5
6
7
8
9
10
11
12
13
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@forEach // 局部返回到该 lambda 表达式的调用者——forEach 循环
print(it)
}
print(" done with implicit label")
}
//sampleEnd

fun main() {
foo()
}

或者,我们用一个匿名函数替代 lambda 表达式。 匿名函数内部的 return 语句将从该匿名函数自身返回

1
2
3
4
5
6
7
8
9
10
11
12
13
//sampleStart
fun foo() {
listOf(1, 2, 3, 4, 5).forEach(fun(value: Int) {
if (value == 3) return // 局部返回到匿名函数的调用者——forEach 循环
print(value)
})
print(" done with anonymous function")
}
//sampleEnd

fun main() {
foo()
}

请注意,前文三个示例中使用的局部返回类似于在常规循环中使用 continue

并没有 break 的直接等价形式,不过可以通过增加另一层嵌套 lambda 表达式并从其中非局部返回来模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//sampleStart
fun foo() {
run loop@{
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@loop // 从传入 run 的 lambda 表达式非局部返回
print(it)
}
}
print(" done with nested loop")
}
//sampleEnd

fun main() {
foo()
}

当要返一个回值的时候,解析器优先选用标签限定的返回:

1
return@a 1

这意味着“返回 1@a”,而不是“返回一个标签标注的表达式 (@a 1)”。

 

基本语法

1
2
3
fun main(){
println("hello world")
}

同时kt代码结尾无须分号。

变量关键字

val:相当于finalvar则是可以重复赋值。

如果变量赋了初始化的值,则其变量类型可以省略,没有赋初值则必须标明数据类型。

1
2
3
4
5
val a: Int = 1
val b = 2
val c: Int
c = 3
//var同理

 

函数

跟java写法风格还是有区别,但是该有的都有

函数体可以是表达式。其返回类型可以推断出来。

1
fun sum(a: Int, b: Int) = a + b

无返回的值,相当于java的void:

1
2
3
fun printSum(a: Int, b: Int): Unit {
println("sum of $a and $b is ${a + b}")
}

Unit 返回类型可以省略。

1
2
3
4
//sampleStart
fun printSum(a: Int, b: Int) {
println("sum of $a and $b is ${a + b}")
}

函数参数可以有默认值,当省略相应的参数时使用默认值。这可以减少重载数量:

1
2
3
4
5
6
7
fun read(
b: Array<Int>,
off: Int = 0,
len: Int = b.size,
) { }
//调用样例
read(arrayOf(1,2,3), off = 0)

覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个有默认参数值的方法时,必须从签名中省略默认参数值:

1
2
3
4
5
6
7
open class A {
open fun foo(i: Int = 10) { /*……*/ }
}

class B : A() {
override fun foo(i: Int) { /*……*/ } // 不能有默认值。
}

如果一个默认参数在一个无默认值的参数之前,那么该默认值只能通过使用具名参数调用该函数来使用:

1
2
3
4
5
6
fun foo(
bar: Int = 0,
baz: Int,
) { /*……*/ }
//也就是说你要显示地指出这个不使用默认值的参数名
foo(baz = 1) // 使用默认值 bar = 0

如果在默认参数之后的最后一个参数是 lambda 表达式,那么它既可以作为具名参数在括号内传入,也可以在括号外传入(当然,在括号外传入在风格上感觉怪怪的):

1
2
3
4
5
6
7
8
9
fun foo(
bar: Int = 0,
baz: Int = 1,
qux: () -> Unit,
) { /*……*/ }

foo(1) { println("hello") } // 使用默认值 baz = 1
foo(qux = { println("hello") }) // 使用两个默认值 bar = 0 与 baz = 1
foo { println("hello") } // 使用两个默认值 bar = 0 与 baz = 1

 

可变数量的参数(varargs)

也不怎么常用,感觉

函数的参数(通常是最后一个)可以用 vararg 修饰符标记:

1
2
3
4
5
6
fun <T> asList(vararg ts: T): List<T> {
val result = ArrayList<T>()
for (t in ts) // ts is an Array
result.add(t)
return result
}

在本例中,可以将可变数量的参数传递给函数:

1
val list = asList(1, 2, 3)

在函数内部,类型 Tvararg 参数的可见方式是作为 T 数组,如上例中的 ts 变量具有类型 Array <out T>

只有一个参数可以标注为 vararg如果 vararg 参数不是列表中的最后一个参数, 可以使用具名参数语法传递其后的参数的值,或者,如果参数具有函数类型,则通过在括号外部传一个 lambda

当调用 vararg-函数时,可以逐个传参,例如 asList(1, 2, 3)如果已经有一个数组并希望将其内容传给该函数,那么使用伸展(spread)操作符(在数组前面加 *

1
2
val a = arrayOf(1, 2, 3)
val list = asList(-1, 0, *a, 4)

If you want to pass a primitive type array into vararg, you need to convert it to a regular (typed) array using the toTypedArray() function:

如果要将基元类型数组传递给 vararg,则需要使用 toTypedArray ()函数将其转换为常规(类型化)数组:

1
2
val a = intArrayOf(1, 2, 3) // IntArray is a primitive type array
val list = asList(-1, 0, *a.toTypedArray(), 4)

 

中缀表示法

java中没有,而且感觉会破坏代码风格

标有 infix 关键字的函数也可以使用中缀表示法(忽略该调用的点与圆括号)调用。 中缀函数必须满足以下要求:

1
2
3
4
5
6
7
infix fun Int.shl(x: Int): Int { …… }

// 用中缀表示法调用该函数
1 shl 2

// 等同于这样
1.shl(2)

中缀函数调用的优先级低于算术操作符、类型转换以及 rangeTo 操作符。 以下表达式是等价的:

  • 1 shl 2 + 3 等价于 1 shl (2 + 3)
  • 0 until n * 2 等价于 0 until (n * 2)
  • xs union ys as Set<*> 等价于 xs union (ys as Set<*>)

另一方面,中缀函数调用的优先级高于布尔操作符 &&||is-in- 检测以及其他一些操作符。这些表达式也是等价的:

  • a && b xor c 等价于 a && (b xor c)
  • a xor b in c 等价于 (a xor b) in c

请注意,中缀函数总是要求指定接收者与参数。当使用中缀表示法在当前接收者上调用方法时,需要显式使用 this。这是确保非模糊解析所必需的。

1
2
3
4
5
6
7
8
9
class MyStringCollection {
infix fun add(s: String) { /*……*/ }

fun build() {
this add "abc" // 正确
add("abc") // 正确
//add "abc" // 错误:必须指定接收者
}
}

 

局部函数

Kotlin 支持局部函数,即一个函数在另一个函数内部:

1
2
3
4
5
6
7
8
9
fun dfs(graph: Graph) {
fun dfs(current: Vertex, visited: MutableSet<Vertex>) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
}

dfs(graph.vertices[0], HashSet())
}

局部函数可以访问外部函数(闭包)的局部变量。在上例中,visited 可以是局部变量:

1
2
3
4
5
6
7
8
9
10
fun dfs(graph: Graph) {
val visited = HashSet<Vertex>()
fun dfs(current: Vertex) {
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
}

dfs(graph.vertices[0])
}

尾递归函数

只是一种简写方式,条件较多,并非不可替代。

 

高阶函数与 lambda 表达式

Kotlin 函数都是头等的,这意味着它们可以存储在变量与数据结构中,并可以作为参数传给其他高阶函数(高阶函数是将函数用作参数或返回值的函数)以及从其他高阶函数返回。可以像操作任何其他非函数值一样对函数进行操作。

高阶函数样例:

此函数接受一个初始累积值与一个接合函数,并通过将当前累积值与每个集合元素连续接合起来代入累积值来构建返回值:

1
2
3
4
5
6
7
8
9
10
fun <T, R> Collection<T>.fold(
initial: R,
combine: (acc: R, nextElement: T) -> R
): R {
var accumulator: R = initial
for (element: T in this) {
accumulator = combine(accumulator, element)
}
return accumulator
}

在上述代码中,参数 combine 具有函数类型 (R, T) -> R,因此 fold 接受一个函数作为参数, 该函数接受类型分别为 RT 的两个参数并返回一个 R 类型的值。 在 for 循环内部调用该函数,然后将其返回值赋值给 accumulator

为了调用 fold,需要传给它一个函数类型的实例作为参数, 而在高阶函数调用处,lambda 表达式广泛用于此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun main() {
//sampleStart
val items = listOf(1, 2, 3, 4, 5)

// Lambdas 表达式是花括号括起来的代码块。
items.fold(0) {
// 如果一个 lambda 表达式有参数,前面是参数,后跟“->”
acc: Int, i: Int ->
print("acc = $acc, i = $i, ")
val result = acc + i
println("result = $result")
// lambda 表达式中的最后一个表达式是返回值:
result
}

// lambda 表达式的参数类型是可选的,如果能够推断出来的话:
val joinedToString = items.fold("Elements:") { acc, i -> "$acc $i" }

// 函数引用也可以用于高阶函数调用:
val product = items.fold(1, Int::times)//相当于累乘
//sampleEnd
println("joinedToString = $joinedToString")
println("product = $product")
}

 

lambda表达式

Lambda 表达式语法

Lambda 表达式的完整语法形式如下:

1
val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }
  • lambda 表达式总是括在花括号中。
  • 完整语法形式的参数声明放在花括号内,并有可选的类型标注。
  • 函数体跟在一个 -> 之后
  • 如果推断出的该 lambda 的返回类型不是 Unit,那么该 lambda 主体中的最后一个(或可能是单个)表达式会视为返回值

如果将所有可选标注都去除,看起来如下:

1
val sum = { x: Int, y: Int -> x + y }

 

传递末尾的 lambda 表达式

按照 Kotlin 惯例,如果函数的最后一个参数是函数,那么作为相应参数传入的 lambda 表达式可以放在圆括号之外(IDEA系列的IDE也都是这么推荐的):

1
val product = items.fold(1) { acc, e -> acc * e }

这种语法也称为拖尾 lambda 表达式

如果该 lambda 表达式是调用时唯一的参数,那么圆括号可以完全省略

1
run { println("...") }

 

it:单个参数的隐式名称

一个 lambda 表达式只有一个参数很常见。

If the compiler can parse the signature without any parameters, the parameter does not need to be declared and -> can be omitted. 该参数会隐式声明为 it

1
ints.filter { it > 0 } // 这个字面值是“(it: Int) -> Boolean”类型的

 

从 lambda 表达式中返回一个值

可以使用限定的返回语法从 lambda 显式返回一个值。 否则,将隐式返回最后一个表达式的值。

因此,以下两个片段是等价的:

1
2
3
4
5
6
7
8
9
ints.filter {
val shouldFilter = it > 0
shouldFilter//返回值
}

ints.filter {
val shouldFilter = it > 0
return@filter shouldFilter//返回值
}

这一约定连同在圆括号外传递 lambda 表达式一起支持 LINQ-风格 的代码:

1
strings.filter { it.length == 5 }.sortedBy { it }.map { it.uppercase() }

 

下划线用于未使用的变量

如果 lambda 表达式的参数未使用,那么可以用下划线取代其名称:

1
map.forEach { _, value -> println("$value!") }

 

内联函数

java不支持直接声明为内联函数,cpp可能会见得多一些,如不熟可跳过

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。 闭包那些在函数体内会访问到的变量的作用域。 内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销

但是在许多情况下通过内联化 lambda 表达式可以消除这类的开销。 下述函数是这种情况的很好的例子。lock() 函数可以很容易地在调用处内联。 考虑下面的情况:

1
lock(l) { foo() }

编译器没有为参数创建一个函数对象并生成一个调用。取而代之,编译器可以生成以下代码:

1
2
3
4
5
6
l.lock()
try {
foo()
} finally {
l.unlock()
}

为了让编译器这么做,需要使用 inline 修饰符标记 lock() 函数:

1
inline fun <T> lock(lock: Lock, body: () -> T): T { …… }

inline 修饰符影响函数本身和传给它的 lambda 表达式:所有这些都将内联到调用处

内联可能导致生成的代码增加。不过如果使用得当(避免内联过大函数), 性能上会有所提升,尤其是在循环中的“超多态(megamorphic)”调用处。

 

noinline

如果不希望内联所有传给内联函数的 lambda 表达式参数都内联,那么可以用 noinline 修饰符标记不希望内联的函数参数

1
inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { …… }

可以内联的 lambda 表达式只能在内联函数内部调用或者作为可内联的参数传递。 但是 noinline 的 lambda 表达式可以以任何喜欢的方式操作,包括存储在字段中、或者进行传递。

如果一个内联函数没有可内联的函数参数并且没有具体化的类型参数,编译器会产生一个警告,因为内联这样的函数很可能并无益处(如果你确认需要内联, 那么可以用 @Suppress("NOTHING_TO_INLINE") 注解关掉该警告)。

 

非局部返回

在 Kotlin 中,只能对具名或匿名函数使用正常的、非限定的 return 来退出。 要退出一个 lambda 表达式,需要使用一个标签。 在 lambda 表达式内部禁止使用裸 return,因为 lambda 表达式不能使包含它的函数返回

1
2
3
4
5
6
7
8
9
10
11
12
13
fun ordinaryFunction(block: () -> Unit) {
println("hi!")
}
//sampleStart
fun foo() {
ordinaryFunction {
return // 错误:不能使 `foo` 在此处返回
}
}
//sampleEnd
fun main() {
foo()
}

但是如果 lambda 表达式传给的函数是内联的,该 return 也可以内联。因此可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
inline fun inlined(block: () -> Unit) {
println("hi!")
}
//sampleStart
fun foo() {
inlined {
return // OK:该 lambda 表达式是内联的//这个return就是lambda表达式,如果不理解请往上翻
}
}
//sampleEnd
fun main() {
foo()
}

这种返回(位于 lambda 表达式中,但退出包含它的函数)称为非局部返回。 通常会在循环中用到这种结构,其内联函数通常包含:

1
2
3
4
5
6
fun hasZeros(ints: List<Int>): Boolean {
ints.forEach {
if (it == 0) return true // 从 hasZeros 返回
}
return false
}

请注意,一些内联函数可能调用传给它们的不是直接来自函数体、而是来自另一个执行上下文的 lambda 表达式参数,例如来自局部对象或嵌套函数。在这种情况下,该 lambda 表达式中也不允许非局部控制流。若要指示内联函数的 lambda 参数不能使用非本地返回值,请用修饰符 crossinline 标记 lambda 参数:

1
2
3
4
5
6
inline fun f(crossinline body: () -> Unit) {
val f = object: Runnable {
override fun run() = body()
}
// ……
}

breakcontinue 在内联的 lambda 表达式中还不可用,但我们也计划支持它们。

比如这样是非法的:

1
2
3
4
5
6
7
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) continue
print(it)
}
print(" done with explicit label")
}

这看起来确实让人不那么舒服,原因和解决方案我们在流程控制中讲过,比如可以这样

1
2
3
4
5
6
7
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者——forEach 循环
print(it)
}
print(" done with explicit label")
}

 

具体化的类型参数

有时候需要访问一个作为参数传递的一个类型:

1
2
3
4
5
6
7
8
fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? {
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p.parent
}
@Suppress("UNCHECKED_CAST")
return p as T?
}

在这里向上遍历一棵树并且检测每个节点是不是特定的类型。 这都没有问题,但是调用处不是很优雅:

1
treeNode.findParentOfType(MyTreeNode::class.java)

更好的解决方案是只要传一个类型给该函数,可以按以下方式调用它:

1
treeNode.findParentOfType<MyTreeNode>()

为能够这么做,内联函数支持具体化的类型参数,于是可以这样写:

1
2
3
4
5
6
7
inline fun <reified T> TreeNode.findParentOfType(): T? {
var p = parent
while (p != null && p !is T) {
p = p.parent
}
return p as T?
}

上述代码使用 reified 修饰符来限定类型参数使其可以在函数内部访问它, 几乎就像是一个普通的类一样。由于函数是内联的,不需要反射,正常的操作符如 !isas 现在均可用。此外,还可以按照如上所示的方式调用该函数:myTree.findParentOfType<MyTreeNodeType>()

虽然在许多情况下可能不需要反射,但仍然可以对一个具体化的类型参数使用它:

1
2
3
4
5
inline fun <reified T> membersOf() = T::class.members

fun main(s: Array<String>) {
println(membersOf<StringBuilder>().joinToString("\n"))
}

普通的函数(未标记为内联函数的)不能有具体化参数。 不具有运行时表示的类型(例如非具体化的类型参数或者类似于 Nothing 的虚构类型)不能用作具体化的类型参数的实参。

 

内联属性

inline 修饰符可用于没有幕后字段的属性的访问器。 你可以标注独立的属性访问器:

1
2
3
4
5
6
val foo: Foo
inline get() = Foo()

var bar: Bar
get() = ……
inline set(v) { …… }

你也可以标注整个属性,将它的两个访问器都标记为内联(inline):

1
2
3
inline var bar: Bar
get() = ……
set(v) { …… }

在调用处,内联访问器如同内联函数一样内联。

 

公有 API 内联函数的限制

当一个内联函数是 publicprotected 而不是 privateinternal 声明的一部分时, 就会认为它是一个模块级的公有 API。可以在其他模块中调用它,并且也可以在调用处内联这样的调用。

这带来了一些由模块做这样变更时导致的二进制兼容的风险—— 声明一个内联函数但调用它的模块在它修改后并没有重新编译。

为了消除这种由非公有 API 变更引入的不兼容的风险,公有 API 内联函数体内不允许使用非公有声明,即,不允许使用 privateinternal 声明以及其部件。

一个 internal 声明可以由 @PublishedApi 标注,这会允许它在公有 API 内联函数中使用。 当一个 internal 内联函数标记有 @PublishedApi 时,也会像公有函数一样检测其函数体。

 

操作符重载

https://book.kotlincn.net/text/operator-overloading.html

类与对象

可见性修饰符

类、对象、接口、构造函数、方法与属性及其 setter 都可以有可见性修饰符。 getter 总是与属性有着相同的可见性。

在 Kotlin 中有这四个可见性修饰符:privateprotectedinternalpublic。 默认可见性是 public

  • private 意味着只该成员在这个类内部(包含其所有成员)可见;
  • protected 意味着该成员具有private 一样的可见性,但也在子类中可见
  • internal 意味着能见到类声明的本模块内的任何客户端都可见其 internal 成员。
    • 一个模块是编译在一起的一套 Kotlin 文件
    • 个人感觉在小项目内跟public没什么区别
  • public 意味着能见到类声明的任何客户端都可见其 public 成员。

在 Kotlin 中,外部类不能访问内部类的 private 成员。

如果你覆盖一个 protectedinternal 成员并且没有显式指定其可见性,该成员还会具有与原始成员相同的可见性。

 

构造函数

跟Java区别还是不小

而且kt进行类的实例化不需要new

在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数主构造函数是类头的一部分:它跟在类名与可选的类型参数后

1
class Person constructor(firstName: String) { /*……*/ }

如果主构造函数没有任何注解或者可见性修饰符可以省略这个 constructor 关键字

1
class Person(firstName: String) { /*……*/ }

主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中

在实例初始化期间,初始化块按照它们出现在类体中的顺序执行,与属性初始化器交织在一起:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//sampleStart
class InitOrderDemo(name: String) {
val firstProperty = "First property: $name".also(::println)

init {
println("First initializer block that prints $name")
}

val secondProperty = "Second property: ${name.length}".also(::println)

init {
println("Second initializer block that prints ${name.length}")
}
}
//sampleEnd

fun main() {
InitOrderDemo("hello")
}

输出:

1
2
3
4
First property: hello
First initializer block that prints hello
Second property: 5
Second initializer block that prints 5

 

次构造函数

类也可以声明前缀有 constructor的次构造函数:

1
2
3
4
5
6
7
class Person(val pets: MutableList<Pet> = mutableListOf())

class Pet {
constructor(owner: Person) {
owner.pets.add(this) // adds this pet to the list of its owner's pets
}
}

如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可

1
2
3
4
5
6
class Person(val name: String) {
val children: MutableList<Person> = mutableListOf()
constructor(name: String, parent: Person) : this(name) {
parent.children.add(this)
}
}

请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行

即使该类没有主构造函数,这种委托仍会隐式发生,并且仍会执行初始化块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//sampleStart
class Constructors {
init {
println("Init block")
}

constructor(i: Int) {
println("Constructor $i")
}
}
//sampleEnd

fun main() {
Constructors(1)
}

//Init block
//Constructor 1

如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。

如果你不希望你的类有一个公有构造函数,那么声明一个带有非默认可见性的空的主构造函数:

1
class DontCreateMe private constructor () { /*……*/ }

在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成一个额外的无参构造函数,它将使用默认值。这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。

1
class Customer(val customerName: String = "")

 

继承

在 Kotlin 中所有类都有一个共同的超类 Any,对于没有超类型声明的类它是默认超类:

1
class Example // 从 Any 隐式继承

Any 有三个方法:equals()hashCode()toString()。因此,为所有 Kotlin 类都定义了这些方法。

默认情况下,Kotlin 类是最终(final)的——它们不能被继承。 要使一个类可继承,请用 open 关键字标记它

1
open class Base // 该类开放继承

如需声明一个显式的超类型,请在类头中把超类型放到冒号之后:

1
2
3
open class Base(p: Int)

class Derived(p: Int) : Base(p)//类似cpp的风格

如果派生类有一个主构造函数,其基类可以(并且必须)将其参数在该主构造函数中初始化

如果派生类没有主构造函数,那么每个次构造函数必须使用super 关键字初始化其基类型,或委托给另一个做到这点的构造函数。 请注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数:

1
2
3
4
5
class MyView : View {
constructor(ctx: Context) : super(ctx)

constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

 

覆盖方法

Kotlin 对于可覆盖的成员以及覆盖后的成员需要显式修饰符(关键字):

1
2
3
4
5
6
7
8
open class Shape {
open fun draw() { /*……*/ }
fun fill() { /*……*/ }
}

class Circle() : Shape() {
override fun draw() { /*……*/ }
}

Circle.draw() 函数上必须加上 override 修饰符。如果没写,编译器会报错。 如果函数没有标注 openShape.fill(),那么子类中不允许定义相同签名的函数, 无论加不加 override。将 open 修饰符添加到 final 类(即没有 open 的类) 的成员上不起作用。

标记为 override 的成员本身是开放的,因此可以在子类中覆盖。如果你想禁止再次覆盖, 使用 final 关键字

1
2
3
open class Rectangle() : Shape() {
final override fun draw() { /*……*/ }
}

 

覆盖属性

属性与方法的覆盖机制相同。在超类中声明然后在派生类中重新声明的属性必须以 override 开头,并且它们必须具有兼容的类型。 每个声明的属性可以由具有初始化器的属性或者具有 get 方法的属性覆盖:

1
2
3
4
5
6
7
open class Shape {
open val vertexCount: Int = 0
}

class Rectangle : Shape() {
override val vertexCount = 4
}

你也可以用一个 var 属性覆盖一个 val 属性,但反之则不行。 这是允许的,因为一个 val 属性本质上声明了一个 get 方法, 而将其覆盖为 var 只是在子类中额外声明一个 set 方法

请注意,你可以在主构造函数中使用 override 关键字作为属性声明的一部分:

1
2
3
4
5
6
7
8
9
interface Shape {
val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // 总是有 4 个顶点

class Polygon : Shape {
override var vertexCount: Int = 0 // 以后可以设置为任何数
}

 

派生类初始化顺序

在构造派生类的新实例的过程中,第一步完成其基类的初始化 (在之前只有对基类构造函数参数的求值),这意味着它发生在派生类的初始化逻辑运行之前。

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
//sampleStart
open class Base(val name: String) {

init { println("Initializing a base class") }

open val size: Int =
name.length.also { println("Initializing size in the base class: $it") }
}

class Derived(
name: String,
val lastName: String,
) : Base(name.replaceFirstChar { it.uppercase() }.also { println("Argument for the base class: $it") }) {

init { println("Initializing a derived class") }

override val size: Int =
(super.size + lastName.length).also { println("Initializing size in the derived class: $it") }
}
//sampleEnd

fun main() {
println("Constructing the derived class(\"hello\", \"world\")")
Derived("hello", "world")
}

运行结果:

1
2
3
4
5
6
Constructing the derived class("hello", "world")
Argument for the base class: Hello
Initializing a base class
Initializing size in the base class: 5
Initializing a derived class
Initializing size in the derived class: 10

这意味着,基类构造函数执行时,派生类中声明或覆盖的属性都还没有初始化。在基类初始化逻辑中(直接或者通过另一个覆盖的 open 成员的实现间接)使用任何一个这种派生类属性,都可能导致不正确的行为或运行时故障。 设计一个基类时,应该避免在构造函数、属性初始化器或者 init 块中使用 open 成员

 

调用超类实现

派生类中的代码可以使用 super 关键字调用其超类的函数与属性访问器的实现,这与java一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
override fun draw() {
super.draw()
println("Filling the rectangle")
}

val fillColor: String get() = super.borderColor
}

在一个内部类中访问外部类的超类,可以使用由外部类名限定的 super 关键字来实现:super@Outer

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
open class Rectangle {
open fun draw() { println("Drawing a rectangle") }
val borderColor: String get() = "black"
}

//sampleStart
class FilledRectangle: Rectangle() {
override fun draw() {
val filler = Filler()
filler.drawAndFill()
}

inner class Filler {
fun fill() { println("Filling") }
fun drawAndFill() {
super@FilledRectangle.draw() // 调用 Rectangle 的 draw() 实现
fill()
println("Drawn a filled rectangle with color ${[email protected]}") // 使用 Rectangle 所实现的 borderColor 的 get()
}
}
}
//sampleEnd

fun main() {
val fr = FilledRectangle()
fr.draw()
}

 

覆盖规则

在 Kotlin 中,实现继承由下述规则规定:如果一个类从它的直接超类继承相同成员的多个实现, 它必须覆盖这个成员并提供其自己的实现(也许用继承来的其中之一)。

如需表示采用从哪个超类型继承的实现,请使用由尖括号中超类型名限定的 super ,如 super<Base>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
open class Rectangle {
open fun draw() { /* …… */ }
}

interface Polygon {
fun draw() { /* …… */ } // 接口成员默认就是“open”的
}

class Square() : Rectangle(), Polygon {
// 编译器要求覆盖 draw():
override fun draw() {
super<Rectangle>.draw() // 调用 Rectangle.draw()
super<Polygon>.draw() // 调用 Polygon.draw()
}
}

可以同时继承 RectanglePolygon, 但是二者都有各自的 draw() 实现,所以必须在 Square 中覆盖 draw(), 并为其提供一个单独的实现以消除歧义。

 

属性

属性的简单声明跟声明一个变量并没有太大区别。

Getter 与 Setter

声明一个属性的完整语法如下:

1
2
3
var <propertyName>[: <PropertyType>] [= <property_initializer>]
[<getter>]
[<setter>]

初始器(initializer)、getter 和 setter 都是可选的。属性类型如果可以从初始器, 或其 getter 的返回值(如下文所示)中推断出来,也可以省略:

1
var initialized = 1 // 类型 Int、默认 getter 和 setter

一个只读属性的语法和一个可变的属性的语法有两方面的不同: 1、只读属性的用 val 而不是 var 声明 2、只读属性不允许 setter。

可以为属性定义自定义的访问器。如果定义了一个自定义的 getter,那么每次访问该属性时都会调用它 (这让可以实现计算出的属性)。以下是一个自定义 getter 的示例:

1
2
3
4
5
6
7
8
9
10
//sampleStart
class Rectangle(val width: Int, val height: Int) {
val area: Int // property type is optional since it can be inferred from the getter's return type
get() = this.width * this.height
}
//sampleEnd
fun main() {
val rectangle = Rectangle(3, 4)
println("Width=${rectangle.width}, height=${rectangle.height}, area=${rectangle.area}")
}

如果可以从 getter 推断出属性类型,则可以省略它:

1
val area get() = this.width * this.height

如果定义了一个自定义的 setter,那么每次给属性赋值时都会调用它, except its initialization. 一个自定义的 setter 如下所示:

1
2
3
4
5
var stringRepresentation: String
get() = this.toString()
set(value) {
setDataFromString(value) // 解析字符串并赋值给其他属性
}

按照惯例,setter 参数的名称是 value,但是如果你喜欢你可以选择一个不同的名称。

下面这个我刚开始也有点懵,确实在语法上与java区别比较大

如果你需要改变对一个访问器进行注解或者改变其可见性,但是不需要改变默认的实现, 你可以定义访问器而不定义其实现:

1
2
3
4
5
var setterVisibility: String = "abc"
private set // 此 setter 是私有的并且有默认实现

var setterWithAnnotation: Any? = null
@Inject set // 用 Inject 注解此 setter

 

幕后字段

这个很有意思

在 Kotlin 中,字段仅作为属性的一部分在内存中保存其值时使用。字段不能直接声明。 然而,当一个属性需要一个幕后字段时,Kotlin 会自动提供。这个幕后字段可以使用 field 标识符在访问器中引用:

1
2
3
4
5
6
var counter = 0 // 这个初始器直接为幕后字段赋值
set(value) {
if (value >= 0)
field = value
// 不能使用counter = value这样的代码 // ERROR StackOverflow: Using actual name 'counter' would make setter recursive
}

field 标识符只能用在属性的访问器内。

如果属性至少一个访问器使用默认实现, 或者自定义访问器通过 field 引用幕后字段,将会为该属性生成一个幕后字段。

例如,定义如下类:

1
2
3
4
5
class Person {
var name
get() = "wavever"
set(value) = print(value)
}

反编译看下 Java 代码:

1
2
3
4
5
6
7
8
9
10
public final class Person {

public final String getName() {
return "wavever";
}

public final void setName(@NotNull String value) {
System.out.print(value);
}
}

生成的 getName 和 setName 中的内容与之前定义的一致,但好像哪里不太对。。属性 name 去哪了?难道是因为声明的时候自定义了 getter 和 setter,但没有加初始值,所以给省略了么?话不多说,加上试试:

1
2
3
4
5
class Person {
var name = "wavever" // 报错
get() = "wavever"
set(value) = print(value)
}

很不幸,编译器提示有错误,看看报错信息:

1
Initializer is not allowed here because this property has no backing field

也就是说,没有幕后字段时不允许进行初始化

这个时候再反编译一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Person {

public final String getName() {
return "wavever";
}

public final void setName(@NotNull String value) {
System.out.print(value);
}

public Person() {
this.name = "wavever";
}
}

诶。。。看到属性 name 了,但还缺少声明它的地方,结合之前的报错信息,那应该就是和这个 backing field 有点关系了。

backing field 即幕后字段,结合 Kotlin 文档来看,当 getter 和 setter 有一个为默认实现,或者在 getter 和 setter 中通过 filed 标识符引用幕后字段时,才会自动生成幕后字段,怎么理解呢?再看下上边的例子,如果我们改为:

1
2
3
4
class Person {
var name = "wavever"
set(value) = print(value)
}

或是

1
2
3
4
5
class Person {
var name = "wavever"
get() = "name is $field"
set(value) = print(value)
}

可以发现都不会再报错,反编译后,可以看到也有变量 name 生成:

1
2
3
4
5
6
7
8
9
10
11
12
public final class Person {

private String name = "wavever";

public final String getName() {
return "name is " + this.name;
}

public final void setName(@NotNull String value) {
System.out.print(value);
}
}

看到这里其实就很清楚了,幕后字段在有默认访问器的情况下,需要生成访问器,访问器里必定需要使用字段,而当自定义访问器里需要使用字段值时,也必须有该字段,否则就会存在在 getter 里调用 getter 这种递归调用的情况了。

参考https://wavever.github.io/2020/03/25/%E7%90%86%E8%A7%A3-Koltin-%E4%B8%AD%E7%9A%84%E5%B9%95%E5%90%8E%E5%AD%97%E6%AE%B5%E5%92%8C%E5%B9%95%E5%90%8E%E5%B1%9E%E6%80%A7/

 

延迟初始化属性与变量

一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检测。

为处理这种情况,你可以用 lateinit 修饰符标记该属性:

1
2
3
4
5
6
7
8
9
10
11
public class MyTest {
lateinit var subject: TestSubject

@SetUp fun setup() {
subject = TestSubject()
}

@Test fun test() {
subject.method() // 直接解引用
}
}

该修饰符只能用于在类体中的属性(不是在主构造函数中声明的 var 属性, 并且仅当该属性没有自定义 getter 或 setter 时),也用于顶层属性与局部变量。 该属性或变量必须为非空类型,并且不能是原生类型。

在初始化前访问一个 lateinit 属性会抛出一个特定异常,该异常明确标识该属性被访问及它没有初始化的事实。

检测一个 lateinit var 是否已初始化

要检测一个 lateinit var 是否已经初始化过,请在该属性的引用上使用 .isInitialized

1
2
3
if (foo::bar.isInitialized) {
println(foo.bar)
}

此检测仅对可词法级访问的属性可用,当声明位于同一个类型内、位于其中一个外围类型中或者位于相同文件的顶层的属性时。

 

接口

接口的逻辑与java基本相同。可以既包含抽象方法的声明也包含实现。与抽象类不同的是,接口无法保存状态。它可以有属性但必须声明为抽象或提供访问器实现,在接口中声明的属性不能有幕后字段(backing field),因此接口中声明的访问器不能引用它们。同样使用关键字 interface 来定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface MyInterface {
val prop: Int // 抽象的

val propertyWithImplementation: String
get() = "foo"

fun foo() {
print(prop)
}
}

class Child : MyInterface {
override val prop: Int = 29
override val propertyWithImplementation: String
get() = super.propertyWithImplementation
override fun foo() {
super.foo()
print("child foo,foo=${propertyWithImplementation}")
}
}

 

接口继承

类似java,一个接口可以从其他接口派生,意味着既能提供基类型成员的实现也能声明新的函数与属性。很自然地,实现这样接口的类只需定义所缺少的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Named {
val name: String
}

interface Person : Named {
val firstName: String
val lastName: String

override val name: String get() = "$firstName $lastName"
}

//数据类,后面有讲
data class Employee(
// 不必实现“name”
override val firstName: String,
override val lastName: String,
val position: String
) : Person

 

覆盖冲突

实现多个接口时,可能会遇到实现同名方法的问题(跟继承那一部分讲的覆盖规则是一样的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface A {
fun foo() { print("A") }
fun bar()
}

interface B {
fun foo() { print("B") }
fun bar() { print("bar") }
}

class C : A {
override fun bar() { print("bar") }
}

class D : A, B {
override fun foo() {
super<A>.foo()
super<B>.foo()
}

override fun bar() {
super<B>.bar()
}
}

上例中,接口 AB 都定义了方法 foo()bar()。 两者都实现了 foo(), 但是只有 B 实现了 bar() (bar()A 中标记为抽象, 因为在接口中没有方法体时默认为抽象)。 现在,如果实现 A 的一个具体类 C,那么必须要重写 bar() 并实现这个抽象方法。

然而,如果从 AB 派生 D,需要实现从多个接口继承的所有方法,并指明 D 应该如何实现它们。这一规则既适用于继承单个实现(bar())的方法也适用于继承多个实现(foo())的方法。

 

函数式(SAM)接口与转换

只有一个抽象方法的接口称为函数式接口或 单一抽象方法(SAM)接口。函数式接口可以有多个非抽象成员,但只能有一个抽象成员。

可以用 fun 修饰符在 Kotlin 中声明一个函数式接口

1
2
3
fun interface KRunnable {
fun invoke()
}
SAM 转换

对于函数式接口,可以通过 lambda 表达式实现 SAM 转换,从而使代码更简洁、更有可读性。

使用 lambda 表达式可以替代手动创建实现函数式接口的类。 通过 SAM 转换, Kotlin can convert any lambda expression whose signature matches the signature of the interface’s single method into the code, which dynamically instantiates the interface implementation.

例如,有这样一个 Kotlin 函数式接口:

1
2
3
fun interface IntPredicate {
fun accept(i: Int): Boolean
}

如果不使用 SAM 转换,那么你需要像这样编写代码:

1
2
3
4
5
6
// 创建一个类的实例
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}

通过利用 Kotlin 的 SAM 转换,可以改为以下等效代码:

1
2
// 通过 lambda 表达式创建一个实例
val isEven = IntPredicate { it % 2 == 0 }

 

扩展

Kotlin 能够扩展一个类的新功能而无需继承该类。 例如,你可以为一个你不能修改的来自第三方库中的类编写一个新的函数。 这个新增的函数就像那个原始类本来就有的函数一样,可以用普通的方法调用。 这种机制称为 扩展函数 。此外,也有 扩展属性 , 允许你为一个已经存在的类添加新的属性。

扩展方法

Kotlin的扩展函数允许存在类成员进行调用的函数定义在这个类的外部。这样可以很方便的扩展一个已经存在的类,为它添加额外的方法。在Kotlin源码中,有大量的扩展函数来扩展Java,这样使得Kotlin比Java更方便使用,效率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Jump {
fun test() {
println("jump test") //在被扩展的类中使用
doubleJump(1f)
}
}

fun Jump.doubleJump(howLong: Float): Boolean {
println("jump:$howLong")
println("jump:$howLong")
return true
}
Jump().doubleJump(2f)
//在被扩展类的外部使用
Jump().test()

详细可以参阅https://doc.devio.org/as/book/docs/Part1/Android%E5%BC%80%E5%8F%91%E5%BF%85%E5%A4%87Kotlin%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF/KotlinExtensions.html

 

数据类

数据类就是一些只保存数据的类,类似于DTO,但是是用来保存数据的。用关键字data在类前标记。

1
data class User(val name: String, val age: Int)

编译器自动从主构造函数中声明的所有属性导出以下成员:

  • equals()/hashCode()
  • toString() 格式是 "User(name=John, age=42)"
  • componentN() 函数 按声明顺序对应于所有属性
  • copy() 函数

为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:

  • 主构造函数需要至少有一个参数
  • 主构造函数的所有参数需要标记为 valvar
  • 数据类不能是抽象、开放(open)、密封(sealed,后面讲)或者内部(指不能是内部类,但可以是internal )的

此外,数据类成员的生成遵循关于成员继承的这些规则:

  • 如果在数据类体中有显式实现 equals()hashCode() 或者 toString(),或者这些函数在父类中有 final 实现,那么不会生成这些函数,而会使用现有函数。
  • 如果超类型具有 opencomponentN() 函数并且返回兼容的类型, 那么会为数据类生成相应的函数,并覆盖超类的实现。如果超类型的这些函数由于签名不兼容或者是 final 而导致无法覆盖,那么会报错。
  • 不允许为 componentN() 以及 copy() 函数提供显式实现。

数据类可以扩展其他类(示例请参见密封类)。

在 JVM 中,如果生成的类需要含有一个无参的构造函数,那么属性必须指定默认值。

 

在类体中声明的属性

对于那些自动生成的函数,编译器只使用在主构造函数内部定义的属性。 如需在生成的实现中排除一个属性,请将其声明在类体中:

1
2
3
data class Person(val name: String) {
var age: Int = 0
}

toString()equals()hashCode() 以及 copy() 的实现中只会用到 name 属性, 并且只有一个 component 函数 component1()。虽然两个 Person 对象可以有不同的年龄, 但它们会视为相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data class Person(val name: String) {
var age: Int = 0
}
fun main() {
//sampleStart
val person1 = Person("John")
val person2 = Person("John")
person1.age = 10
person2.age = 20
//sampleEnd
println("person1 == person2: ${person1 == person2}")
println("person1 with age ${person1.age}: ${person1}")
println("person2 with age ${person2.age}: ${person2}")
}

 

密封类

原文英文,我自己翻译了一下

密封类和接口表示受限制的类层次结构,它们提供对继承的更多控制。密封类的所有直接子类在编译时都是已知的。在编译具有密封类的模块之后,不能出现其他子类。例如,第三方客户端不能在其代码中扩展您的密封类。因此,密封类的每个实例都有一个来自有限集合的类型,类型在这个类在编译时是已知的(Thus, each instance of a sealed class has a type from a limited set that is known when this class is compiled)。

密封接口及其实现也是如此,一旦编译了实现了密封接口的模块,就不会出现新的实现(The same works for sealed interfaces and their implementations: once a module with a sealed interface is compiled, no new implementations can appear)。

在某种意义上,密封类类似于枚举类: 枚举类型的值集也受到限制,但是每个枚举常量只作为一个实例存在,而一个密封类的子类可以有多个实例,每个实例都有自己的状态。

例如,考虑一个库的 API。它可能包含错误类,让库用户处理它可能抛出的错误。如果此类错误类的层次结构包括在公共 API 中可见的接口或抽象类,那么没有什么可以阻止在客户端代码中实现或扩展它们。然而,库不知道在它之外声明的错误,所以它不能用自己的类一致地处理这些错误。使用密封的错误类层次结构,库作者可以确保他们知道所有可能的错误类型,以后不会出现其他错误类型。

若要声明一个密封的类或接口,请将修饰符sealed放在其名称之前:

1
2
3
4
5
6
7
8
sealed interface Error

sealed class IOError(): Error

class FileReadError(val file: File): IOError()
class DatabaseError(val source: DataSource): IOError()

object RuntimeError : Error

一个密封类是自身抽象的,它不能直接实例化并可以有抽象(abstract)成员。

Constructors of sealed classes can have one of two visibilities: protected (by default) or private:

1
2
3
4
5
sealed class IOError {
constructor() { /*...*/ } // protected by default
private constructor(description: String): this() { /*...*/ } // private is OK
// public constructor(code: Int): this() {} // Error: public and internal are not allowed
}

Sealed classes and when expression

使用密封类的关键好处在于使用 when 表达式的时候。 如果能够验证语句覆盖了所有情况,就不需要为该语句再添加一个 else 子句了。 当然,这只有当你用 when 作为表达式(使用结果)而不是作为语句时才有用。

1
2
3
4
5
6
fun log(e: Error) = when(e) {
is FileReadError -> { println("Error while reading file ${e.file}") }
is DatabaseError -> { println("Error while reading from database ${e.source}") }
RuntimeError -> { println("Runtime error") }
// 不再需要 `else` 子句,因为已经覆盖了所有的情况
}

 

型变

阅读这一部分前建议先深入了解了解java的泛型机制,虽然直接去学kt也并没有太大难度但是如果你想进行两者的对比从而从中收获些什么的话,还是建议多少学一下。

所以java的泛型问题在哪呢?类型擦除

Java 类型系统中最棘手的部分之一是通配符类型, 而 Kotlin 中没有。 相反,Kotlin 有声明处型变(declaration-site variance)与类型投影(type projections)。

我们来思考下为什么 Java 需要这些神秘的通配符。在 《Effective Java》第三版 很好地解释了该问题—— 第 31 条:利用有限制通配符来提升 API 的灵活性。 首先,Java 中的泛型是不型变的,这意味着 List<String> 并不是 List<Object> 的子类型。 如果 List 不是不型变的,它就没比 Java 的数组好到哪去,因为如下代码会通过编译但是导致运行时异常:

1
2
3
4
5
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs; // !!!此处的编译器错误让我们避免了之后的运行时异常。
objs.add(1); // 将一个整数放入一个字符串列表
String s = strs.get(0); // !!! ClassCastException:无法将整数转换为字符串

Java 禁止这样的事情以保证运行时的安全。但这样会有一些影响。例如, 考虑 Collection 接口中的 addAll() 方法。该方法的签名应该是什么?直觉上, 需要这样写:

1
2
3
4
// Java
interface Collection<E> …… {
void addAll(Collection<E> items);
}

但随后,就无法做到以下这样(完全安全的)的事:

1
2
3
4
5
6
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
// !!!对于这种简单声明的 addAll 将不能编译:
// Collection<String> 不是 Collection<Object> 的子类型
}

这就是为什么 addAll() 的实际签名是以下这样:

1
2
3
4
// Java
interface Collection<E> …… {
void addAll(Collection<? extends E> items);
}

通配符类型参数 ? extends E 表示此方法接受 E 或者 E 的一个子类型对象的Collection集合,而不只是 E 自身。 这意味着我们可以安全地从其中 (该集合中的元素是 E 的子类的实例)读取 E,但不能写入, 因为我们不知道什么对象符合那个未知的 E 的子类型。 反过来,该限制可以得到想要的行为:Collection<String> 表示为 Collection<? extends Object> 的子类型。 简而言之,带 extends 限定的通配符类型使得类型是协变的(covariant)。

理解为什么这能够工作的关键相当简单:如果只能从集合中获取元素, 那么使用 String 的集合, 并且从其中读取 Object 也没问题 。反过来,如果只能向集合中 放入 元素 , 就可以用 Object 集合并向其中放入 String:在 Java 中有 List<? super String>List<Object> 的一个超类。

后者称为逆变性(contravariance),并且对于 List <? super String> 你只能调用接受 String 作为参数的方法 (例如,你可以调用 add(String) 或者 set(int, String)),如果调用函数返回 List<T> 中的 T, 你得到的并非一个 String 而是一个 Object

Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些只能向其写入的对象为消费者。他建议:

“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”, 并提出了以下助记符:

PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。

如果你使用一个生产者对象,如 List<? extends Foo>,在该对象上不允许调用 add()set(), 但这并不意味着它是不可变的:例如,没有什么阻止你调用 clear() 从列表中删除所有元素,因为 clear() 根本无需任何参数。

通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。

声明处型变

假设有一个泛型接口 Source<T>,该接口中不存在任何以 T 作为参数的方法,只是方法返回 T 类型值:

1
2
3
4
// Java
interface Source<T> {
T nextT();
}

那么,在 Source <Object> 类型的变量中存储 Source <String> 实例的引用是极为安全的—— 没有消费者-方法可以调用。但是 Java 并不知道这一点,并且仍然禁止这样操作:

1
2
3
4
5
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许,因为泛型是不型变的
// ……
}

为了修正这一点,我们必须声明对象的类型为 Source<? extends Object>。这么做毫无意义, 因为我们可以像以前一样在该对象上调用所有相同的方法,所以更复杂的类型并没有带来价值。 但编译器并不知道。

在 Kotlin 中,有一种方法向编译器解释这种情况。这称为声明处型变: 可以标注 Source 的类型参数T 来确保它仅从 Source<T> 成员中返回(生产),并从不被消费。 为此请使用 out 修饰符

1
2
3
4
5
6
7
8
interface Source<out T> {
fun nextT(): T
}

fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out参数
// ……
}

一般原则是:当一个类 C 的类型参数 T 被声明为 out 时,它就只能出现在 C 的成员的输出位置, 但回报是 C<Base> 可以安全地作为 C<Derived> 的超类

简而言之,可以说类 C 是在参数 T 上是协变的,或者说 T 是一个协变的类型参数。 可以认为 CT 的生产者,而不是 T 的消费者

out 修饰符称为型变注解,并且由于它在类型参数声明处提供, 所以它提供了声明处型变。 这与 Java 的使用处型变相反,其类型用途通配符使得类型协变。

另外除了 out,Kotlin 又补充了一个型变注解:in。它使得一个类型参数逆变,即只可以消费而不可以生产。逆变类型的一个很好的例子是 Comparable

1
2
3
4
5
6
7
8
9
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}

fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}

 

类型投影

使用处型变:类型投影

将类型参数 T 声明为 out 非常简单,并且能避免使用处子类型化的麻烦, 但是有些类实际上不能限制为只返回 T! 一个很好的例子是 Array

1
2
3
4
class Array<T>(val size: Int) {
operator fun get(index: Int): T { …… }
operator fun set(index: Int, value: T) { …… }
}

该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:

1
2
3
4
5
fun copy(from: Array<Any>, to: Array<Any>) {
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
}

这个函数应该将项目从一个数组复制到另一个数组。让我们尝试在实践中应用它:

1
2
3
4
val ints: Array<Int> = arrayOf(1, 2, 3)
val any = Array<Any>(3) { "" }
copy(ints, any)
// 其类型为 Array<Int> 但此处期望 Array<Any>

这里我们遇到同样熟悉的问题:Array <T>T 上是不型变的,因此 Array <Int>Array <Any> 都不是另一个的子类型。为什么? 再次重复,因为 copy 可能有非预期行为,例如它可能尝试写一个 Stringfrom, 并且如果我们实际上传递一个 Int 的数组,以后会抛 ClassCastException 异常。

1
2
To prohibit the `copy` function from writing to `from`, you can do the following:
fun copy(from: Array<out Any>, to: Array<Any>) { …… }

这就是类型投影:意味着 from 不仅仅是一个数组,而是一个受限制的(投影的)数组。 只可以调用返回类型为类型参数 T 的方法,如上,这意味着只能调用 get()。 这就是使用处型变的用法,并且是对应于 Java 的 Array<? extends Object>、 但更简单。

你也可以使用 in 投影一个类型:

1
fun fill(dest: Array<in String>, value: String) { …… }

Array<in String> 对应于 Java 的 Array<? super String>,也就是说,你可以传递一个 CharSequence 数组或一个 Object 数组给 fill() 函数。

 

星投影

这个确实没怎么看懂

有时你想说,你对类型参数一无所知,但仍然希望以安全的方式使用它。 这里的安全方式是定义泛型类型的这种投影,该泛型类型的每个具体实例化都会是该投影的子类型。

Kotlin 为此提供了所谓的星投影语法:

  • 对于 Foo <out T : TUpper>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。
  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 意味着当 T 未知时, 没有什么可以以安全的方式写入 Foo <*>
  • 对于 Foo <T : TUpper>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于Foo<in Nothing>

如果泛型类型具有多个类型参数,则每个类型参数都可以单独投影。 例如,如果类型被声明为 interface Function <in T, out U>,可以使用以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

星投影非常像 Java 的原始类型,但是安全。

 

泛型函数

基本与java一样,可略过

不仅类可以有类型参数。函数也可以有。类型参数要放在函数名称之前

1
2
3
4
5
6
7
fun <T> singletonList(item: T): List<T> {
// ……
}

fun <T> T.basicToString(): String { // 扩展函数
// ……
}

要调用泛型函数,在调用处函数名之后指定类型参数即可:

1
val l = singletonList<Int>(1)

可以省略能够从上下文中推断出来的类型参数,所以以下示例同样适用:

1
val l = singletonList(1)

 

泛型约束

基本与java逻辑相同

 

嵌套类与内部类

用到的不多而且逻辑跟java很像,很容易理解,也可以略过

类可以嵌套在其他类中:

1
2
3
4
5
6
7
8
class Outer {
private val bar: Int = 1
class Nested {
fun foo() = 2
}
}

val demo = Outer.Nested().foo() // == 2

还可以使用带有嵌套的接口。所有类与接口的组合都是可能的:可以将接口嵌套在类中、将类嵌套在接口中、将接口嵌套在接口中。

1
2
3
4
5
6
7
8
9
interface OuterInterface {
class InnerClass
interface InnerInterface
}

class OuterClass {
class InnerClass
interface InnerInterface
}

内部类

标记为 inner 的嵌套类能够访问其外部类的成员。内部类会带有一个对外部类的对象的引用:

1
2
3
4
5
6
7
8
class Outer {
private val bar: Int = 1
inner class Inner {
fun foo() = bar
}
}

val demo = Outer().Inner().foo() // == 1

 

枚举类

枚举类的最基本的应用场景是实现类型安全的枚举:

1
2
3
enum class Direction {
NORTH, SOUTH, WEST, EAST
}

每个枚举常量都是一个对象。枚举常量以逗号分隔。

java中枚举类就是一组常量

每一个枚举都是枚举类的实例,可以这样初始化:

1
2
3
4
5
enum class Color(val rgb: Int) {
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
}

匿名类

枚举常量可以声明其带有相应方法以及覆盖了基类方法的自身匿名类 。

1
2
3
4
5
6
7
8
9
10
11
enum class ProtocolState {
WAITING {
override fun signal() = TALKING
},

TALKING {
override fun signal() = WAITING
};

abstract fun signal(): ProtocolState
}

如果枚举类定义任何成员,那么使用分号将成员定义与常量定义分隔开。

 

在枚举类中实现接口

一个枚举类可以实现接口(但不能从类继承),可以为所有条目提供统一的接口成员实现,也可以在相应匿名类中为每个条目提供各自的实现。 只需将想要实现的接口添加到枚举类声明中即可,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.function.BinaryOperator
import java.util.function.IntBinaryOperator

//sampleStart
enum class IntArithmetics : BinaryOperator<Int>, IntBinaryOperator {
PLUS {
override fun apply(t: Int, u: Int): Int = t + u
},
TIMES {
override fun apply(t: Int, u: Int): Int = t * u
};

override fun applyAsInt(t: Int, u: Int) = apply(t, u)
}
//sampleEnd

fun main() {
val a = 13
val b = 31
for (f in IntArithmetics.values()) {
println("$f($a, $b) = ${f.apply(a, b)}")
}
}

 

使用枚举常量

Kotlin 中的枚举类也有合成方法用于列出定义的枚举常量以及通过名称获取枚举常量。这些方法的签名如下(假设枚举类的名称是 EnumClass):

1
2
EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>

如果指定的名称与类中定义的任何枚举常量均不匹配, valueOf() 方法会抛出 IllegalArgumentException 异常。

可以使用 enumValues<T>()enumValueOf<T>() 函数以泛型的方式访问枚举类中的常量:

1
2
3
4
5
6
7
enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
print(enumValues<T>().joinToString { it.name })
}

printAllValues<RGB>() // 输出 RED, GREEN, BLUE

每个枚举常量都具有在枚举类声明中获取其名称与位置的属性:

1
2
val name: String
val ordinal: Int

 

内联类

比如我有这样的类或者数据类:

1
2
3
4
5
class User1 {
val username : String = "name"
}

data class User2(val username : String)

这种数据包装类效率很低,而且占内存。因为这个类实际上只包装了一个String的数据,但是因为他是一个单独声明的类,所以如果实例化的话还需要单独给这个类创建一个实例,放在jvmheap 内存里。

如果有一种办法,既可以让这个数据类保持它单独的类型,又不那么占空间,那么可以考虑内联类。

1
value class User(private val username: String)

一些使用规定:

  • 如果编译目标包括 JVM,还需要添加 @JvmInline 注解,否则无法通过编译

  • 只能提供主构造器。

  • 和 data class 类似,需要定义构造器参数

  • 可以在主构造器中定义有且仅有一个只读属性 (var 不行,只能 val)

  • 内联类不能继承类或被继承,但是可以继承接口

  • 内联类可以实现接口

内联类支持普通类中的一些功能。特别是,内联类可以声明属性与函数以及init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@JvmInline
value class Name(val s: String) {
init {
require(s.length > 0) { }
}

val length: Int
get() = s.length

fun greet() {
println("Hello, $s")
}
}

fun main() {
val name = Name("Kotlin")
name.greet() // `greet` 方法会作为一个静态方法被调用
println(name.length) // 属性的 get 方法会作为一个静态方法被调用
}

内联类属性不能有幕后字段。它们只能有简单的可计算属性(没有 lateinit/委托属性)。

 

继承

内联类允许去继承接口

1
2
3
4
5
6
7
8
9
10
11
12
13
interface Printable {
fun prettyPrint(): String
}

@JvmInline
value class Name(val s: String) : Printable {
override fun prettyPrint(): String = "Let's $s!"
}

fun main() {
val name = Name("Kotlin")
println(name.prettyPrint()) // 仍然会作为一个静态方法被调用
}

内联类不能继承其他的类而且必须是 final(也不能被继承)。

 

表示方式

在生成的代码中,Kotlin 编译器为每个内联类保留一个包装器。内联类的实例可以在运行时表示为包装器或者基础类型。这就类似于 Int 可以表示为原生类型 int 或者包装器 Integer

为了生成性能最优的代码,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
interface I

@JvmInline
value class Foo(val i: Int) : I

fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}

fun <T> id(x: T): T = x

fun main() {
val f = Foo(42)

asInline(f) // 拆箱操作: 用作 Foo 本身
asGeneric(f) // 装箱操作: 用作泛型类型 T
asInterface(f) // 装箱操作: 用作类型 I
asNullable(f) // 装箱操作: 用作不同于 Foo 的可空类型 Foo?

// 在下面这里例子中,'f' 首先会被装箱(当它作为参数传递给 'id' 函数时)然后又被拆箱(当它从'id'函数中被返回时)
// 最后, 'c' 中就包含了被拆箱后的内部表达(也就是 '42'), 和 'f' 一样
val c = id(f)
}

因为内联类既可以表示为基础类型有可以表示为包装器,引用相等对于内联类而言毫无意义,因此这也是被禁止的。

 

别名

类型别名为现有类型提供替代名称。 如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。

它有助于缩短较长的泛型类型。 例如,通常缩减集合类型是很有吸引力的:

1
2
3
typealias NodeSet = Set<Network.Node>

typealias FileTable<K> = MutableMap<K, MutableList<File>>

你可以为函数类型提供另外的别名:

1
2
3
typealias MyHandler = (Int, String, Any) -> Unit

typealias Predicate<T> = (T) -> Boolean

你可以为内部类和嵌套类创建新名称:

1
2
3
4
5
6
7
8
9
class A {
inner class Inner
}
class B {
inner class Inner
}

typealias AInner = A.Inner
typealias BInner = B.Inner

类型别名不会引入新类型。 它们等效于相应的底层类型。

 

内联类与类型别名

初看起来,内联类似乎与类型别名非常相似。然而,关键的区别在于类型别名与其基础类型(以及具有相同基础类型的其他类型别名)是赋值兼容的,而内联类却不是这样

换句话说,内联类引入了一个真实的新类型,与类型别名正好相反,类型别名仅仅是为现有的类型取了个新的替代名称 (别名):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typealias NameTypeAlias = String

@JvmInline
value class NameInlineClass(val s: String)

fun acceptString(s: String) {}
fun acceptNameTypeAlias(n: NameTypeAlias) {}
fun acceptNameInlineClass(p: NameInlineClass) {}

fun main() {
val nameAlias: NameTypeAlias = ""
val nameInlineClass: NameInlineClass = NameInlineClass("")
val string: String = ""

acceptString(nameAlias) // 正确: 传递别名类型的实参替代函数中基础类型的形参
acceptString(nameInlineClass) // 错误: 不能传递内联类的实参替代函数中基础类型的形参

// And vice versa:
acceptNameTypeAlias(string) // 正确: 传递基础类型的实参替代函数中别名类型的形参
acceptNameInlineClass(string) // 错误: 不能传递基础类型的实参替代函数中内联类类型的形参
}

 

对象表达式与对象声明

Java中,不管是为了实现接口,或者是抽象类,我们总是习惯使用匿名内部类。最熟悉的例子,莫过于对单击事件的监听.

1
2
3
btn.setOnClickListener(new OnClickListener{
// 处理单击事件逻辑
});

Kotlin没有匿名内部类,对应的,考虑对象表达式。

对象表达式

对象表达式以object关键字开头,最简单的,比如:

1
2
3
4
5
6
btn.setOnClickListener(object : OnClickListener{...});//这其实是一个继承
//或者
val listener = object {
//对象表达式内的代码可以访问创建这个对象的代码范围内的变量,与Java不同的是,被访问的变量不需要被限制为final变量
fun doClick() {...}
}

如需创建一个继承自某个(或某些)类型的匿名类的对象,在object和冒号(:)之后指定此类型。然后实现或重写这个类的成员,就像从它继承一样:

1
2
3
4
5
window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { /*……*/ }

override fun mouseEntered(e: MouseEvent) { /*……*/ }
})

如果超类型有一个构造函数,那么传递适当的构造函数参数给它。 多个超类型可以由跟在冒号后面的逗号分隔的列表指定:

1
2
3
4
5
6
7
8
9
open class A(x: Int) {
public open val y: Int = x
}

interface B { /*……*/ }

val ab: A = object : A(1), B {
override val y = 15
}

当匿名对象被用作局部或私有的类型,但不是内联声明(函数或属性)时,其所有成员都可以通过此函数或属性访问:

1
2
3
4
5
6
7
8
9
class C {
private fun getObject() = object {
val x: String = "x"
}

fun printX() {
println(getObject().x)
}
}

如果此函数或属性为公共或私有内联,则其实际类型为:

  • 如果匿名对象没有声明超类,则为Any

  • 匿名对象的声明的超类型(如果确实存在这样的类型)

  • 如果有多个声明的超类型,则为显式声明的类型(The explicitly declared type if there is more than one declared supertype)

在所有这些情况下,无法访问添加到匿名对象中的成员。如果重写成员是在函数或属性的实际类型中声明的,则可以访问它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface A {
fun funFromA() {}
}
interface B

class C {
// The return type is Any. x is not accessible
fun getObject() = object {
val x: String = "x"
}

// The return type is A; x is not accessible
fun getObjectA() = object: A {
override fun funFromA() {}
val x: String = "x"
}

// The return type is B; funFromA() and x are not accessible
fun getObjectB(): B = object: A, B { // explicit return type is required
override fun funFromA() {}
val x: String = "x"
}
}

匿名对象的变量访问

对象表达式中的代码可以访问来自包含它的作用域的变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fun countClicks(window: JComponent) {
var clickCount = 0
var enterCount = 0

window.addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
clickCount++
}

override fun mouseEntered(e: MouseEvent) {
enterCount++
}
})
// ……
}

 

对象声明

对象声明,我们可以理解为 java 中的单例模式:

1
2
3
4
5
6
7
8
object DataProviderManager {
fun registerDataProvider(provider: DataProvider) {
// ……
}

val allDataProviders: Collection<DataProvider>
get() = // ……
}

这称为对象声明。并且它总是在 object 关键字后跟一个名称。 就像变量声明一样,对象声明不是一个表达式,不能用在赋值语句的右边。

对象声明的初始化过程是线程安全的并且在首次访问时进行。

如需引用该对象,直接使用其名称即可:

1
DataProviderManager.registerDataProvider(……)

这些对象可以有超类型:

1
2
3
4
5
object DefaultListener : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) { …… }

override fun mouseEntered(e: MouseEvent) { …… }
}

对象声明不能在局部作用域(即不能直接嵌套在函数内部),但是它们可以嵌套到其他对象声明或非内部类中。对象声明在另一个类的内部时,这个对象并不能通过外部类的实例访问到该对象,而只能通过类名来访问 同样该对象也不能直接访问到外部类的方法和变量

使用时完全符合单例,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
object Site {
var url:String = ""
val name: String = "ymc"
}
fun main(args: Array<String>) {
var s1 = Site
var s2 = Site
s1.url = "www.google.com"
println(s1.url)
println(s2.url)
}

//输出:
//www.google.com
//www.google.com

 

伴生对象

Kotlin中没有static的概念,之所以能抛弃静态成员,主要原因在于它允许包级属性和函数的存在,而且 Kotlin 为了维持与 Java 完全的兼容性,为静态成员提供了多种替代方案:

  1. 使用 包级属性和包级函数:主要用于 全局常量工具函数
  2. 使用 伴生对象:主要用于与类有紧密联系的变量和函数;
  3. 使用 @JvmStatic 注解:与伴生对象搭配使用,将变量和函数声明为真正的 JVM 静态成员。

类内部的对象声明可以用 companion 关键字标记,这样它就与外部类关联在一起,我们就可以直接通过外部类访问到对象的内部元素。当然我们也可以省略对象的对象名,使用 Companion 来代替。看起来很像java中的静态方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
companion object Factory {
fun create(): MyClass = MyClass()
}
}

val instance = MyClass.create() // 伴生对象的成员可通过只使用类名作为限定符来调用
//等价于
val instance = MyClass.Factory.create()

//--------------------------
// 也可以这样写
class MyClass2 {
companion object {}
}

val x = MyClass2.Companion //直接使用 companion 代替

**注意:**一个类里面只能声明一个内部关联对象,即关键字 companion 只能使用一次。

其自身所用的类的名称可用作该类的伴生对象的引用:

1
2
3
4
5
6
7
8
9
class MyClass1{
companion object Named{ }
}
val x = MyClass1

class MyClass2{
companion object{ }
}
val y = MyClass2

请伴生对象的成员看起来像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
interface Factory<T>{
fun create():T
}

class MyClass{
companion object : Factory<MyClass>{
override fun create():MyClass = MyClass()

var name = "default"
val myClass = MyClass()
fun sayClass(){
println("hello say")
}
}

fun sayHello(){
println("hello")
}
}

fun main(arg:Array<String>){
MyClass.sayClass()
val f:Factory<MyClass> = MyClass
val myClass = f.create()
myClass.sayHello()
}

我的反编译代码:

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
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.jvm.internal.Intrinsics;
import org.jetbrains.annotations.NotNull;

interface Factory {
Object create();
}

final class TestKt {
public static void main(@NotNull String[] arg) {
Intrinsics.checkNotNullParameter(arg, "arg");
MyClass.Companion.sayClass();
Factory f = (Factory)MyClass.Companion;
MyClass myClass = (MyClass)f.create();
myClass.sayHello();
}
}

final class MyClass {
@NotNull
private static String name = "default";
@NotNull
private static final MyClass myClass = new MyClass();
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

public final void sayHello() {
String var1 = "hello";
System.out.println(var1);
}

public static final class Companion implements Factory {
@NotNull
public MyClass create() {
return new MyClass();
}

// $FF: synthetic method
// $FF: bridge method
//这段反编译的代码我注释掉才能跑
// public Object create() {
// return this.create();
// }

@NotNull
public final String getName() {
return MyClass.name;
}

public final void setName(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
MyClass.name = var1;
}

@NotNull
public final MyClass getMyClass() {
return MyClass.myClass;
}

public final void sayClass() {
String var1 = "hello say";
System.out.println(var1);
}

private Companion() {
}

// $FF: synthetic method
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}

使用伴生对象实际上是在这个类内部创建了一个名为 Companion静态单例内部类

伴生对象中定义的属性会直接编译为外部类的私有静态字段,var和val的区别就是无有final

函数会被编译为伴生对象的方法

 

伴生对象的扩展

如果一个类定义有一个伴生对象,也可以为伴生对象定义扩展函数与属性,就像伴生对象的常规成员一样,可以只使用类名作为限定符来调用伴生对象的扩展成员:

1
2
3
4
5
6
7
8
9
10
11
class MyClass{
companion object{ }
}

fun MyClass.Companion.printCompanion(){
print("companion")
}

fun main(){
MyClass.printCompanion()
}

 

委托

https://juejin.cn/post/6958346113552220173

  • 类委托: 一个类的方法不在该类中定义,而是直接委托给另一个对象来处理。
  • 属性委托: 一个类的属性不在该类中定义,而是直接委托给另一个对象来处理。
  • 局部变量委托: 一个局部变量不在该方法中定义,而是直接委托给另一个对象来处理。

类委托

Kotlin 类委托的语法格式如下:

1
class <类名>(b : <基础接口>) : <基础接口> by <基础对象>

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 基础接口
interface Base {
fun print()
}

// 基础对象
class BaseImpl(val x: Int) : Base {
override fun print() { print(x) }
}

// 被委托类
class Derived(b: Base) : Base by b

fun main(args: Array<String>) {
val b = BaseImpl(10)
Derived(b).print() // 最终调用了 Base#print()
}

基础类和被委托类都实现同一个接口,编译时生成的字节码中,继承自 Base 接口的方法都会委托给基础对象处理。

属性委托

Kotlin 属性委托的语法格式如下:

1
val/var <属性名> : <类型> by <基础对象>

举例:

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
class Example {
// 被委托属性
var prop: String by Delegate() // 基础对象
}

// 基础类
class Delegate {
private var _realValue: String = "彭"

operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
println("getValue")
return _realValue
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("setValue")
_realValue = value
}
}

fun main(args: Array<String>) {
val e = Example()
println(e.prop) // 最终调用 Delegate#getValue()
e.prop = "Peng" // 最终调用 Delegate#setValue()
println(e.prop) // 最终调用 Delegate#getValue()
}

//输出:
//getValue
//彭
//setValue
//getValue
//Peng

基础类不需要实现任何接口,但必须提供 getValue() 方法,如果是委托可变属性,还需要提供 setValue()。在每个属性委托的实现的背后,Kotlin 编译器都会生成辅助属性并委托给它。 例如,对于属性 prop,会生成「辅助属性」 prop$delegate。 而 prop 的 getter() 和 setter() 方法只是简单地委托给辅助属性的 getValue() 和 setValue() 处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//源码:
class Example {
// 被委托属性
var prop: String by Delegate() // 基础对象
}

//--------------------------------------------------------
//编译器生成的字节码:
class Example {
private val prop$delegate = Delegate()
// 被委托属性
var prop: String
get() = prop$delegate.getValue(this, this:prop)
set(value : String) = prop$delegate.setValue(this, this:prop, value)
}

注意事项:

  • thisRef —— 必须与属性所有者类型相同或者是它的超类型。
  • property —— 必须是类型 KProperty<*> 或其超类型。
  • value —— 必须和属性同类型或者是它的超类型。

局部变量委托

局部变量也可以声明委托,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fun main(args: Array<String>) {
val lazyValue: String by lazy {
println("Lazy Init Completed!")
"Hello World."
}

if (true/*someCondition*/) {
println(lazyValue) // 首次调用
println(lazyValue) // 后续调用

}
}

//输出:
//Lazy Init Completed!
//Hello World.
//Hello World.

后面的以后再看吧

 

空安全

dart逻辑基本一样,用?标注可为空的变量如var a : String? = "123" ,调用时使用?.进行调用如var b : a?.length,明确断言不为空则使用!!var c = a!!.length,若为空则使用某个非空值则用?:比如var d = a.length ?: 0

如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null

1
val aInt: Int? = a as? Int

 

相等性

Kotlin 中有两类相等性:

  • 结构相等(==——用 equals() 检测);
  • 引用相等(===——两个引用指向同一对象)。

结构相等

结构相等由 == 以及其否定形式 != 操作判断。 按照约定,像 a == b 这样的表达式会翻译成:

1
a?.equals(b) ?: (b === null)

如果 a 不是 null 则调用 equals(Any?) 函数,否则(anull)检测 b 是否与null 引用相等。

请注意,当与 null 显式比较时完全没必要优化你的代码: a == null 会被自动转换为 a === null

如需提供自定义的相等检测实现,请覆盖 equals(other: Any?): Boolean 函数。 名称相同但签名不同的函数,如 equals(other: Foo) 并不会影响操作符 ==!= 的相等性检测。

结构相等与 Comparable<……> 接口定义的比较无关,因此只有自定义的 equals(Any?) 实现可能会影响该操作符的行为。

 

协程

提醒:android studio请确保导入了依赖如implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

如果了解过异步(比如flutter async,或者python的异步与协程)的话,这部分应该会比较好理解吧

参考https://www.cnblogs.com/joy99/p/15805916.html,保姆级教程

协程是可挂起计算的实例。它在概念上类似于一个线程,在这个意义上,它需要一个代码块来运行,这个代码块与其余代码并发工作。但是,协程不绑定到任何特定的线程。它可以在一个线程中暂停执行,在另一个线程中恢复执行。

Go、Python 等很多编程语言在语言层面上都实现协程,java 也有三方库实现协程,只是不常用, Kotlin 在语言层面上实现协程,对比 java, 主要还是用来解决异步任务线程切换的痛点。

线程的上下文切换都需要内核参与,而协程的上下文切换,完全由用户去控制,避免了大量的中断参与,减少了线程上下文切换与调度消耗的资源。协程可以被认为是轻量级的线程,但是使用上还是有较大差别。

线程是操作系统层面的概念,协程是语言层面的概念。Kotlin在语言级别并没有实现一种同步机制(锁),还是依靠Kotlin-JVM的提供的Java关键字(如synchronized),即锁的实现还是交给线程处理。因而Kotlin协程本质上只是一套基于原生Java线程池的封装。

Kotlin 协程的核心竞争力在于:它能简化异步并发任务,以同步方式写异步代码。

 

基本使用

基本使用

场景: 开启工作线程执行一段耗时任务,然后在主线程对结果进行处理。

常见的处理方式:

  • 自己定义回调,进行处理
  • 使用 线程/线程池, Callable
  • 线程 Thread(FeatureTask(Callable)).start
  • 线程池 submit(Callable)
  • Android: HandlerAsyncTaskRxjava

使用协程:

1
2
3
4
5
6
7
val coroutineScope = CoroutineScope(Dispatchers.Main)
coroutineScope.launch(Dispatchers.Main) { // 在主线程启动一个协程
val result = withContext(Dispatchers.Default) { // 切换到子线程执行
doSomething() // 耗时任务
}
handResult(result) // 切回到主线程执行
}

这里需要注意的是: Dispatchers.Main 是 Android 里面特有的,如果是java程序里面是用则会抛出异常。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)//懒启动,后文也会涉及
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}

而且如果你留意,会发现Dispatchers.Main其实是CoroutineContext类型的参数,CoroutineContextCoroutineScope后文都会讲

 

创建协程的三种方式

使用 runBlocking 顶层函数创建:

1
2
3
runBlocking {
...
}

使用 GlobalScope 单例对象创建

1
2
3
GlobalScope.launch {
...
}

自行通过 CoroutineContext 创建一个 CoroutineScope 对象

1
2
3
4
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
...
}
  • 方法一通常适用于单元测试的场景,而业务开发中不会用到这种方法,因为它是线程阻塞的。
  • 方法二和使用 runBlocking 的区别在于不会阻塞线程。但在 Android 开发中同样不推荐这种用法,因为它的生命周期会只受整个应用程序的生命周期限制,且不能取消。
  • 方法三是比较推荐的使用方法,我们可以通过 context 参数去管理和控制协程的生命周期(这里的 context和 Android 里的不是一个东西,是一个更通用的概念,会有一个 Android 平台的封装来配合使用)。

 

举例

下面这段代码可能需要使用AS运行而非IDEA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import kotlinx.coroutines.*
fun main() = runBlocking {
val job = launch {
delay(100)
println("hello")
delay(300)
println("world")
}
delay(1300L) //
println("test1")
job.join()
println("test2")
}

//test1
//hello
//world
//test2

在很多情况下,主线程创建并启动子线程,如果子线程中要进行大量的耗时运算,主线程将可能早于子线程结束。如果主线程需要知道子线程的执行结果时,就需要等待子线程执行结束了。主线程可以sleep(xx),但这样的xx时间不好确定,因为子线程的执行时间不确定,join()方法比较合适这个场景。

java主线程的代码块中,如果碰到了t.join()方法,此时主线程需要等待(阻塞),等待子线程结束了(Waits for this thread to die.),才能继续执行t.join()之后的代码块。kt也是类似的逻辑

 

结构性并发

协程遵循结构化并发的原则,这意味着新的协程只能在一个特定的 CoroutineScope 中启动,这个 CoroutineScope 限定了协程的生命周期。所有协程完成后,代码块才真正结束。

在实际的应用程序中,您将启动许多协程。结构化并发确保它们不会丢失,也不会泄漏。外部作用域在其所有子协同程序完成之前不能完成。结构化并发还确保正确报告代码中的任何错误,并且永远不会丢失。

 

代码块抽离

让我们将 launch { ... }中的代码块提取到一个单独的函数中。当对这段代码执行“提取函数”重构时,我们将获得一个带有suspend修饰符的新函数。挂起函数可以像常规函数一样在协程中使用,但是它们的附加特性是它们可以依次使用其他挂起函数(比如本例中的delay)来挂起协程的执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import kotlinx.coroutines.*

//sampleStart
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}

// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
//sampleEnd

 

取消协程

与线程类比,java 线程其实没有提供任何机制来安全地终止线程。

Thread 类提供了一个方法 interrupt()方法,用于中断线程的执行。调用interrupt()方法并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。然后由线程在下一个合适的时机中断自己。

但是协程提供了一个 cancel() 方法来取消作业。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() = runBlocking {
val job = launch {
repeat(1000) { i ->
println("job: test $i ...")
delay(500L)
}
}
delay(1300L) // 延迟一段时间
println("main: ready to cancel!")
job.cancel() // 取消该作业
job.join() // 等待作业执行结束
println("main: Now cancel.")
}

//job: test 0 ...
//job: test 1 ...
//job: test 2 ...
//main: ready to cancel!
//main: Now cancel.

也可以使用函数 cancelAndJoin, 它合并了对 cancel 以及 join 的调用。

问题:
如果先调用 job.join() 后调用 job.cancel() 是是什么情况?

取消是协作的

协程并不是一定能取消,协程的取消是协作的。一段协程代码必须协作才能被取消。所有 kotlinx.coroutines 中的挂起函数都是可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException

如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的。例如

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
fun main() = runBlocking {
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
// 每秒打印消息两次
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: hello ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // 等待一段时间
println("main: ready to cancel!")
job.cancelAndJoin() // 取消一个作业并且等待它结束
println("main: Now cancel.")
}

//job: hello 0 ...
//job: hello 1 ...
//job: hello 2 ...
//main: ready to cancel!
//job: hello 3 ...
//job: hello 4 ...
//main: Now cancel.

可见协程并没有被取消。为了能真正停止协程工作,我们需要定期检查协程是否处于 active 状态。

 

检查 job 状态

一种方法是在 while(i<5) 中添加检查协程状态的代码,代码如下:

1
while (i < 5 && isActive)

这样意味着只有当协程处于 active 状态时,我们工作的才会执行。

另一种方法使用协程标准库中的函数 ensureActive(), 它的实现是这样的:

1
2
3
public fun Job.ensureActive(): Unit {
if (!isActive) throw getCancellationException()
}

代码如下:

1
2
3
4
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
ensureActive()
...
}

ensureActive() 在协程不在 active 状态时会立即抛出异常。

使用 yield()

yield()ensureActive 使用方式一样。yield 会进行的第一个工作就是检查任务是否完成,如果 Job 已经完成的话,就会抛出 CancellationException 来结束协程。yield 应该在定时检查中最先被调用。

1
2
3
4
while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU
yield()
...
}

 

等待协程的执行的结果

对于无返回值的的协程使用 launch() 函数创建,如果需要返回值,则通过 async 函数创建。

使用 async 方法启动 Deferred (也是一种 job), 可以调用它的 await()方法获取执行的结果。

形如下面代码:

1
2
3
4
5
val asyncDeferred = async {
...
}

val result = asyncDeferred.await()

deferred 也是可以取消的,对于已经取消的 deferred 调用 await() 方法,会抛出
JobCancellationException 异常。

同理,在 deferred.await 之后调用 deferred.cancel(), 那么什么都不会发生,因为任务已经结束了。

关于 async 的具体用法后面异步任务再讲。

 

协程的异常处理

由于协程被取消时会抛出 CancellationException ,所以我们可以把挂起函数包裹在 try/catch 代码块中,这样就可以在 finally 代码块中进行资源清理操作了。

 

协程的超时

在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动,使用 withTimeout 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fun main() = runBlocking {
withTimeout(300) {
println("start...")
delay(100)
println("progress 1...")
delay(100)
println("progress 2...")
delay(100)
println("progress 3...")
delay(100)
println("progress 4...")
delay(100)
println("progress 5...")
println("end")
}
}

结果:

1
2
3
4
start...
progress 1...
progress 2...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 300 ms

withTimeout() 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout()。如果有必要,我们需要主动 catch 异常进行处理。

当然,还有另一种方式: 使用 withTimeoutOrNull

withTimeout 是可以有返回值的,执行 withTimeout 函数,会阻塞并等待执行完返回结果或者超时抛出异常。

withTimeoutOrNull 用法与 withTimeout 一样,只是在超时后返回 null 。

 

并发与挂起

使用 async 并发

打印当前线程信息的函数,原文没写,我按照原文输出格式自己写的:

1
2
3
fun printWithThreadInfo( s :String = "" ){
println("thread id: ${Thread.currentThread().id}, thread name: ${Thread.currentThread().name} -->$s")
}

考虑一个场景: 开启多个任务,并发执行,所有任务执行完之后,返回结果,再汇总结果继续往下执行。
针对这种场景,解决方案有很多,比如 java 的 FeatureTask

前面提到有返回值的协程,我们通常使用 async 函数来启动。

这里看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun main() = runBlocking {
val time = measureTimeMillis {
val a = async(Dispatchers.IO) {
printWithThreadInfo()
delay(1000) // 模拟耗时操作
1//返回值
}
val b = async(Dispatchers.IO) {
printWithThreadInfo()
delay(2000) // 模拟耗时操作
2
}
printWithThreadInfo("${a.await() + b.await()}")
printWithThreadInfo("end")
}
printWithThreadInfo("time: $time")
}

//前两个线程id可能不一样,正常
//thread id: 14, thread name: DefaultDispatcher-worker-1 -->
//thread id: 16, thread name: DefaultDispatcher-worker-3 -->
//thread id: 1, thread name: main -->3
//thread id: 1, thread name: main -->end
//thread id: 1, thread name: main -->time: 2028

async 启动一个协程后,调用 await 方法后,会阻塞,等待结果的返回,同样能达到效果。

源码:

CoroutineScope.launch()方法结构逻辑极其相似

1
2
3
4
5
6
7
8
9
10
11
12
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)//懒启动,后文讲
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)//只是在这里有区别,以及返回值的区别
coroutine.start(start, coroutine, block)
return coroutine
}

 

懒启动/lazy start

async 可以通过将 start 参数设置为 CoroutineStart.LAZY 变成惰性的。在这个模式下,调用 await 获取协程执行结果的时候,或者调用 Job 的 start 方法时,协程才会启动。

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
fun main() = runBlocking {
val time = measureTimeMillis {
val a = async(Dispatchers.IO, CoroutineStart.LAZY) {
printWithThreadInfo()
delay(1000) // 模拟耗时操作
1
}
val b = async(Dispatchers.IO, CoroutineStart.LAZY) {
printWithThreadInfo()
delay(2000) // 模拟耗时操作
2
}
a.start()
b.start()
printWithThreadInfo("${a.await() + b.await()}")
printWithThreadInfo("end")
}
printWithThreadInfo("time: $time")
}

//thread id: 16, thread name: DefaultDispatcher-worker-3 -->
//thread id: 15, thread name: DefaultDispatcher-worker-2 -->
//thread id: 1, thread name: main -->3
//thread id: 1, thread name: main -->end
//thread id: 1, thread name: main -->time: 2044

但是如果去掉了两个start(),结果变为:

1
2
3
4
5
thread id: 14, thread name: DefaultDispatcher-worker-1 -->
thread id: 14, thread name: DefaultDispatcher-worker-1 -->
thread id: 1, thread name: main -->3
thread id: 1, thread name: main -->end
thread id: 1, thread name: main -->time: 3043

也就是说,会先启动第一个协程,执行完毕,再启动第二个协程(在同一线程),执行完。相当于顺序执行而非并发。

 

挂起函数

这一部分在上面的代码块抽离中其实有过预告,关于suspend关键字

还是上面的例子,加入我们把任务 a 的计算过程提取成一个函数。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun main() = runBlocking {
val time = measureTimeMillis {
val a = async(Dispatchers.IO) {
calA()
}
val b = async(Dispatchers.IO) {
printWithThreadInfo()
delay(2000) // 模拟耗时操作
2
}
printWithThreadInfo("${a.await() + b.await()}")
printWithThreadInfo("end")
}
printWithThreadInfo("time: $time")
}

fun calA(): Int {
printWithThreadInfo()
delay(1000) // 模拟耗时操作
return 1
}

此时会发现,编译器报错了。

1
delay(1000) // 模拟耗时操作

该行报错为:Suspend function 'delay' should be called only from a coroutine or another suspend function

挂起函数 delay 应该在另一个挂起函数调用。

查看 delay 函数源码:

1
2
3
4
5
6
7
8
9
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
// if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
if (timeMillis < Long.MAX_VALUE) {
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}

可以看到,方法签名用 suspend 修饰,表示该函数是一个挂起函数。解决这个异常,只需要将我们定义的 calA() 方法也用 suspend 修饰,使其变成一个挂起函数。

使用 suspend 关键字修饰的函数成为挂起函数,挂起函数只能在另一个挂起函数,或者协程中被调用。在挂起函数中可以调用普通函数(非挂起函数)

 

协程上下文和作用域

协程上下文 CoroutineContext

协程总是运行在一些以 CoroutineContext 类型为代表的上下文中。协程上下文是各种不同元素的集合。其中主元素是协程中的 Job 以及它的调度器。

协程上下文包含当前协程scope的信息, 比如 JobContinuationInterceptorCoroutineNameCoroutineId。在CoroutineContext中,是用map来存这些信息的,如

1
2
val job = context[Job]
val continuationInterceptor = context[ContinuationInterceptor]

前文基本使用模块我们提到了协程上下文,下面代码中使用的是协程调度器dispatcher,使用该构造器作为了一个上下文(按理来说好像是不能这么用的,毕竟变量类型都不一样),但如果打印contextdispatcher会发现都是java.util.concurrent.ThreadPoolExecutor

我们用这个上下文建立了一个作用域myScope,在这个作用域内执行launch

为啥不是Dispatchers.Main:这是安卓独有的,如果是java程序里面是用则会抛出异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() {
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
val context : CoroutineContext = dispatcher
val myScope = CoroutineScope(context)
val job = myScope.launch {
printWithThreadInfo("job: ${this.coroutineContext[Job]}")
}
printWithThreadInfo("job2: $job")
printWithThreadInfo("job3: ${job[Job]}")
}

fun printWithThreadInfo(s :String = "" ){
println("thread id: ${Thread.currentThread().id}, thread name: ${Thread.currentThread().name} -->$s")
}

thread id: 1, thread name: main -->job2: StandaloneCoroutine{Active}@6f1fba17
thread id: 1, thread name: main -->job3: StandaloneCoroutine{Active}@6f1fba17
thread id: 14, thread name: pool-1-thread-1 -->job: StandaloneCoroutine{Active}@6f1fba17

协程上下文包含一个 协程调度器 (CoroutineDispatcher),它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。

其实所有的协程构建器诸如 launchasync 接收一个可选的 CoroutineContext参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。(或许这就是上面代码使用dispatcher的原因)

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
//sampleStart
launch { // 运行在父协程的上下文中,即 runBlocking 主协程
println("main runBlocking : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Unconfined) { // 不受限的——将工作在主线程中
println("Unconfined : I'm working in thread ${Thread.currentThread().name}")
}
launch(Dispatchers.Default) { // 将会获取默认调度器
println("Default : I'm working in thread ${Thread.currentThread().name}")
}
launch(newSingleThreadContext("MyOwnThread")) { // 将使它获得一个新的线程
println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
}
//sampleEnd
}

当调用 launch { …… } 时不传参数,它从启动了它的 CoroutineScope 中承袭了上下文(以及调度器)。

CoroutineContext最重要的两个信息是 Dispatcher 和 Job, 而 Dispatcher 和 Job 本身又实现了 CoroutineContext 的接口。是其子类。

这个设计就很有意思了。

有时我们需要在协程上下文中定义多个元素。我们可以使用 + 操作符来实现。 比如说,我们可以显式指定一个调度器来启动协程并且同时显式指定一个命名:

1
2
3
launch(Dispatchers.Default + CoroutineName("test")) {
println("I'm working in thread ${Thread.currentThread().name}")
}

这得益于 CoroutineContext 重载了操作符 +

 

协程作用域 CoroutineScope

CoroutineScope 即协程运行的作用域,它的源码如下:

1
2
3
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}

可以看出CoroutineScope的代码很简单,主要作用是提供 CoroutineContext

作用域可以管理其域内的所有协程。一个CoroutineScope可以有许多的子scope。协程内部是通过 CoroutineScope.coroutineContext 自动继承自父协程的上下文。而 CoroutineContext 就是在作用域内为协程进行线程切换的快捷方式。

注意:当使用 GlobalScope 来启动一个协程时,则新协程的作业没有父作业。 因此它与这个启动的作用域无关且独立运作。GlobalScope 包含的是 EmptyCoroutineContext

EmptyCoroutineContext这个上下文相信你在前文的源码中已经见过

  • 一个父协程总是等待所有的子协程执行结束。父协程并不显式的跟踪所有子协程的启动,并且不必使用 Job.join 在最后的时候等待它们。
  • 取消父协程会取消所有的子协程。所以使用 Scope 来管理协程的生命周期。
  • 默认情况下,协程内,某个子协程抛出一个非 CancellationException 异常,未被捕获,会传递到父协程,任何一个子协程异常退出,那么整体都将退出。

 

创建 CoroutineScope

创建一个 CoroutineScope, 只需调用 public fun CoroutineScope(context: CoroutineContext) 方法,传入一个 CoroutineContext 对象。

在协程作用域内,启动一个子协程,默认自动继承父协程的上下文,但在启动时,我们可以指定传入上下文。

1
2
3
4
5
val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
val myScope = CoroutineScope(dispatcher)
myScope.launch {
//...
}

 

SupervisorJob

启动一个协程,默认是实例化的是 Job 类型。该类型下,协程内,某个子协程抛出一个非 CancellationException 异常,未被捕获,会传递到父协程,任何一个子协程异常退出,那么整体都将退出。
为了解决上述问题,可以使用SupervisorJob替代JobSupervisorJobJob基本类似,区别在于不会被子协程的异常所影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private val svJob = SupervisorJob()
private val mDispatcher = newSingleThreadContext("test_dispatcher")

private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
printWithThreadInfo("exceptionHandler: throwable: $throwable")
}

private val svScope = CoroutineScope(svJob + mDispatcher + exceptionHandler)
private val mScope = CoroutineScope(Job() + mDispatcher + exceptionHandler)

svScope.launch {
...
}

// 或者
supervisorScope {
launch {
...
}
}

 

协程并发中的数据同步问题

线程中的数据同步问题

经典例子:

1
2
3
4
5
6
7
8
9
10
var flag = true

fun main() {
Thread {
Thread.sleep(1000)
flag = false
}.start()
while (flag) {
}
}

程序并没有像我们所期待的那样,在一秒之后,退出,而是一直处于循环中。

flag 加上 volatile 关键修饰:

1
2
@Volatile
var flag = true

没有用 volatile 修饰 flag 之前,其改变不具有可见性,一个线程将它的值改变后,另一个线程却 “不知道”,所以程序没有退出。当把变量声明为 volatile 类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile 变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。

参见https://www.zwn-blog.xyz/2022/04/21/java-volatile/

当对非volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。

volatile 修饰的遍历具有如下特性:

  1. 保证此变量对所有的线程的可见性,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存来完成。
  2. 禁止指令重排序优化。
  3. 不会阻塞线程。

如果在 while 循环里加一行打印,即使去掉 volatile 修饰,也可以退出程序,查看 println()源码,最终发现,里面有synchronized修饰的同步代码块,

那么问题来了,synchronized 到底干了什么。

按理说,synchronized 只会保证该同步块中的变量的可见性,发生变化后立即同步到主存,但是,flag 变量并不在同步块中,实际上,JVM对于现代的机器做了最大程度的优化,也就是说,最大程度的保障了线程和主存之间的及时的同步,也就是相当于虚拟机尽可能的帮我们加了个volatile,但是,当CPU被一直占用的时候,同步就会出现不及时,也就出现了后台线程一直不结束的情况。

 

协程中的数据同步问题

看如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Test {
private var count = 0
suspend fun test() = withContext(Dispatchers.IO) {
repeat(100) {
launch {
repeat(1000) {
count++
}
}
}
launch {
delay(3000)
printWithThreadInfo("end count: $count")
}
}
}

fun main() = runBlocking<Unit> {
Test().test()
}

执行输出结果:

1
thread id: 15, thread name: DefaultDispatcher-worker-4 ---> end count: 58059

并不是我们期待的 100000。很明显,协程并发过程中数据不同步造成的。

我们也会尝试使用 volatile 修饰变量,但并不能解决问题。volatile 在并发中保证可见性,但是不保证原子性。 count++ 该运算,包含读、写操作,并非一次原子操作。这样并发情况下,自然得不到期望的结果。

 

协程数据同步的解决方案

方案一

使用具有 incrementAndGet 原子操作的 AtomicInteger 类,private var count = AtomicInteger(),并使用count.incrementAndGet()代替count++

温馨提示,这个类是java类。

原理是CAS指令,参见https://www.zwn-blog.xyz/2022/09/13/CAS%E6%8C%87%E4%BB%A4%E4%B8%8EMESI%E7%BC%93%E5%AD%98%E4%B8%80%E8%87%B4%E6%80%A7%E5%8D%8F%E8%AE%AE/

 

方案二:同步操作

对数据的增加进行同步操作。可以同步计数自增的代码块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test {

private val obj = Any()

private var count = 0
suspend fun test() = withContext(Dispatchers.IO) {
repeat(100) {
launch {
repeat(1000) {
synchronized(obj) { // 同步代码块
count++
}
}
}
}
launch {
delay(3000)
printWithThreadInfo("end count: $count")
}
}
}

或者使用 ReentrantLock 操作。

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
class Test {

private val mLock = ReentrantLock()

private var count = 0
suspend fun test() = withContext(Dispatchers.IO) {
repeat(100) {
launch {
repeat(1000) {
mLock.lock()
try{
count++
} finally {
mLock.unlock()
}
}
}
}
launch {
delay(3000)
printWithThreadInfo("end count: $count")
}
}
}

fun main() = runBlocking<Unit> {
val cos = measureTimeMillis {
Test().test()
}
printWithThreadInfo("cos time: ${cos.toString()}")
}

在协程中的替代品叫做 Mutex, 它具有 lock 和 unlock 方法,关键的区别在于, Mutex.lock() 是一个挂起函数,它不会阻塞当前线程。还有 withLock 扩展函数,可以方便的替代常用的 mutex.lock();try { …… } finally { mutex.unlock() } 模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test {

private val mutex = Mutex()

private var count = 0
suspend fun test() = withContext(Dispatchers.IO) {
repeat(100) {
launch {
repeat(1000) {
mutex.withLock {
count++
}
}
}
}
launch {
delay(3000)
printWithThreadInfo("end count: $count")
}
}
}

 

方案三:Actors

一个 actor 是由协程、 被限制并封装到该协程中的状态以及一个与其它协程通信的 通道 组合而成的一个实体。一个简单的 actor 可以简单的写成一个函数, 但是一个拥有复杂状态的 actor 更适合由类来表示。

有一个 actor 协程构建器,它可以方便地将 actor 的邮箱通道组合到其作用域中(用来接收消息)、组合发送 channel 与结果集对象,这样对 actor 的单个引用就可以作为其句柄持有。

使用 actor 的第一步是定义一个 actor 要处理的消息类。 Kotlin 的密封类很适合这种场景。 我们使用 IncCounter 消息(用来递增计数器)和 GetCounter 消息(用来获取值)来定义 CounterMsg 密封类。 后者需要发送回复。CompletableDeferred 通信原语表示未来可知(可传达)的单个值, 这里被用于此目的。

1
2
3
4
// 计数器 Actor 的各种类型
sealed class CounterMsg
object IncCounter : CounterMsg() // 递增计数器的单向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 携带回复的请求

接下来定义一个函数,使用 actor 协程构建器来启动一个 actor:

1
2
3
4
5
6
7
8
9
10
// 这个函数启动一个新的计数器 actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // actor 状态
for (msg in channel) { // 即将到来消息的迭代器
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}

主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Test {

suspend fun test() = withContext(Dispatchers.IO) {
val counterActor = counterActor() // 创建该 actor
repeat(100) {
launch {
repeat(1000) {
counterActor.send(IncCounter)
}
}
}
launch {
delay(3000)
// 发送一条消息以用来从一个 actor 中获取计数值
val response = CompletableDeferred<Int>()
counterActor.send(GetCounter(response))
println("Counter = ${response.await()}")
counterActor.close() // 关闭该actor
}
}
}

actor 本身执行时所处上下文(就正确性而言)无关紧要。一个 actor 是一个协程,而一个协程是按顺序执行的,因此将状态限制到特定协程可以解决共享可变状态的问题。实际上,actor 可以修改自己的私有状态, 但只能通过消息互相影响(避免任何锁定)。
actor 在高负载下比锁更有效,因为在这种情况下它总是有工作要做,而且根本不需要切换到不同的上下文。

实际上, CoroutineScope.actor()方法返回的是一个 SendChannel对象。Channel 也是 Kotlin 协程中的一部分。

 

通道Channel

概念

Channel 翻译过来为通道或者管道,实际上就是个队列, 是一个面向多协程之间数据传输的 BlockQueue,用于协程间通信。Channel 允许我们在不同的协程间传递数据。形象点说就是不同的协程可以往同一个管道里面写入数据或者读取数据。它是一个和 BlockingQueue 非常相似的概念。区别在于:BlockingQueue 使用 puttake 往队列里面写入和读取数据,这两个方法是阻塞的。而 Channel 使用 sendreceive 两个方法往管道里面写入和读取数据。这两个方法是非阻塞的挂起函数,鉴于此,Channel 的 sendreceive 方法也只能在协程中使用

 

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fun main() = runBlocking {
val channel = Channel<Int>()
launch {
for (x in 1..5) {
channel.send(x * x)
}
}

val iterator = channel.iterator()
while (iterator.hasNext()) {
val next = iterator.next()
println(next)
}
println("Done!")

//或者
for (y in channel) {
println(y)
}
println("Done!")
//对于下面这种代码,最后一行 Done! 不会被打印出来,并且程序没有结束。此时,我们发现,这种方式,实际上是我们一直在等待读取 Channel 中的数据,只要有数据到了,就会被读取到。
}

 

关闭 Channel

我们可以使用 close() 方法关闭 Channel,来表明没有更多的元素将会进入通道。

1
2
3
4
5
6
7
8
9
10
val channel = Channel<Int>()
launch {
// 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送
for (x in 1..5) channel.send(x * x)
channle.close() // 结束发送
}
for (y in channel) {
println(y)
}
println("Done!")

从概念上来讲,调用 close 方法就像向通道发送了一个特殊的关闭指令,这个迭代停止,说明关闭指令已经被接收了。所以这里能够保证所有先前发送出去的原色都能在通道关闭前被接收到。

对于一个 Channel,如果我们调用了它的 close(),它会立即停止接受新元素,也就是说这时候它的 isClosedForSend 会立即返回 true,而由于 Channel 缓冲区的存在,这时候可能还有一些元素没有被处理完,所以要等所有的元素都被读取之后 isClosedForReceive 才会返回 true。

 

Channel 的类型

Channel 是一个接口,它继承了 SendChannelReceiveChannel 两个接口

1
public interface Channel<E> : SendChannel<E>, ReceiveChannel<E>

SendChannel

SendChannel 提供了发射数据的功能,有如下重点接口:

  • send() 是一个挂起函数,将指定的元素发送到此通道,在该通道的缓冲区已满或不存在时挂起调用者。如果通道已经关闭,调用发送时会抛出异常。
  • trySend() 如果不违反其容量限制,则立即将指定元素添加到此通道,并返回成功结果。否则,返回失败或关闭的结果。
  • close() 关闭通道。
  • isClosedForSend() 判断通道是否已经关闭,如果关闭,调用 send 会引发异常。

 

ReceiveChannel

ReceiveChannel 提供了接收数据的功能,有如下重点接口:

  • receive() 如果此通道不为空,则从中检索并删除元素;如果通道为空,则挂起调用者;如果通道为接收而关闭,则引发ClosedReceiveChannel()异常。
  • tryReceive() 如果此通道不为空,则从中检索并删除元素,返回成功结果;如果通道为空,则返回失败结果;如果通道关闭,则返回关闭结果。
  • receiveCatching() 如果此通道不为空,则从中检索并删除元素,返回成功结果;如果通道为空,则返回失败结果;如果通道关闭,则返回关闭的原因。
  • isEmpty() 判断通道是否为空
  • isClosedForReceive() 判断通道是否已经关闭,如果关闭,调用 receive() 会引发异常。
  • cancel(cause: CancellationException? = null) 以可选原因取消接收此频道的剩余元素。此函数用于关闭通道并从中删除所有缓冲发送的元素。
  • iterator() 返回通道的迭代器

 

BroadcastChannel

这个通道和一般的通道区别在于他的每个数据可以被每个作用域全部接收到; 默认的通道一个数据被接收后其他的协程是无法再接收到数据的

广播通道通过全局函数创建对象

1
2
3
public fun <E> BroadcastChannel(capacity: Int): BroadcastChannel<E>
//创建如下,必须指定大小
val broadcastChannel = broadcastChannel<Int>(5)

本身广播通道继承自SendChannel,只能发送数据,通过函数可以拿到接收通道

1
2
3
4
public fun openSubscription(): ReceiveChannel<E>
//使用如下:
val receiveChannel = broadcastChannel.openSubscription()
//这样我们就得到了一个 ReceiveChannel,获取订阅的消息,只需要调用它的 receive。

取消通道

1
public fun cancel(cause: CancellationException? = null)

将Channel转成BroadcastChannel

1
2
3
4
5
6
7
fun <E> ReceiveChannel<E>.broadcast(
capacity: Int = 1,
start: CoroutineStart = CoroutineStart.LAZY
): BroadcastChannel<E>
//如:
val channel = Channel<Int>()
val broadcast = channel.broadcast(3)

通过扩展函数在协程作用域中快速创建一个广播发送通道

1
2
3
4
5
6
7
public fun <E> CoroutineScope.broadcast(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 1,
start: CoroutineStart = CoroutineStart.LAZY,
onCompletion: CompletionHandler? = null,
@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): BroadcastChannel<E>
过时提醒

BroadcastChannel 源码中的说明:

1
Note: This API is obsolete since 1.5.0. It will be deprecated with warning in 1.6.0 and with error in 1.7.0. It is replaced with `SharedFlow`.

BroadcastChannel 对于广播式的任务来说有点太复杂了。使用通道进行状态管理时会出现一些逻辑上的不一致。例如,可以关闭或取消通道。但由于无法取消状态,因此在状态管理中无法正常使用!

BroadcastChannel 被标记为过时了,在 Kotlin 1.6.0 版本中使用将显示警告,在 1.7.0 版本中将显示错误。请使用 SharedFlowStateFlow 替代它。关于 SharedFlowStateFlow 将在下文中讲到。

 

不同类型的 Channel

Kotlin 协程库中定义了多个 Channel 类型,所有channel类型的receive方法都是同样的行为: 如果channel不为空, 接收一个元素, 否则挂起。
它们的主要区别在于:

  • 内部可以存储元素的数量
  • send() 是否可以被挂起

Channel 的不同类型:

  • Rendezvous channel: 与建立大小为零的缓冲通道(Buffered channel)相同。 其中一个功能(send或receive)始终被挂起,直到调用另外一个功能为止。 若是调用了send函数,但消费者没有准备好处理该元素则receive会挂起,而且send也会被挂起。 一样,若是调用了receive函数且通道为空,换句话说,没有准备好发送该元素的的send被挂起-receive也会被挂起。(默认是这个)
  • Unlimited channel: 无限元素,send不被挂起,最接近队列的模拟,若是没有更多的内存,则会抛出OutOfMemoryException
  • Buffered channel: 指定大小,生产者能够将元素发送到此通道,直到达到最大限制。 全部元素都在内部存储。 通道已满时,下一个send呼叫将被挂起,直到出现更多可用空间。
  • Conflated channel: 新元素会覆盖旧元素, receiver只会得到最新元素, send永不挂起。

创建 Channel:

1
2
3
4
val rendezvousChannel = Channel<String>()
val bufferedChannel = Channel<String>(10)
val conflatedChannel = Channel<String>(CONFLATED)
val unlimitedChannel = Channel<String>(UNLIMITED)

 

多协程样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun main() = runBlocking<Unit> {
val channel = Channel<Int>()
launch {
for (x in 1..5) channel.send(x)
}
launch {
delay(30)
for (x in 90..100) channel.send(x)
channel.close()
}

launch {
delay(10)
for (y in channel) {
println(" 1 --> $y")
}
}
launch {
delay(20)
for (y in channel) {
println(" 2 --> $y")
}
}
}

 

Produce

相当于生产者

上面介绍的属于创建Channel对象来发送和接收数据,但是还可以通过扩展函数快速创建并返回一个具备发送数据的ReceiveChannel对象

1
2
3
4
5
public fun <E> CoroutineScope.produce(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0,
@BuilderInference block: suspend ProducerScope<E>.() -> Unit
): ReceiveChannel<E>
  • context: 可以通过协程上下文决定调度器等信息
  • capacity: 初始化通道空间

ProducerScope 该接口继承自SendChannel以及CoroutineScope, 具备发送通道数据以及协程作用域作用

produce作用域执行完成会关闭通道, 前面已经提及关闭通道无法继续接收数据

等待取消

该函数会在通道被取消时回调其函数参数, 前面提及协程取消时可以通过finally来释放内存等操作, 但是通道取消无法使用finally只能使用该函数

1
2
public suspend fun ProducerScope<*>.awaitClose(block: () -> Unit = {}) 
// [SendChannel.close] or [ReceiveChannel.cancel] 代表取消通道

 

Actor

相当于消费者

可以通过actor函数创建一个具备通道作用的协程作用域

1
2
3
4
5
6
7
public fun <E> CoroutineScope.actor(
context: CoroutineContext = EmptyCoroutineContext,
capacity: Int = 0, // todo: Maybe Channel.DEFAULT here?
start: CoroutineStart = CoroutineStart.DEFAULT,
onCompletion: CompletionHandler? = null,
block: suspend ActorScope<E>.() -> Unit
): SendChannel<E>
  • context: 协程上下文
  • capacity: 通道缓存空间
  • start: 协程启动模式
  • onCompletion: 完成回调
  • block: 回调函数中可以进行发送数据

该函数和produce函数相似,

  1. produce返回ReceiveChannel, 外部进行数据接收; actor返回的SendChannel, 外部进行数据发送
  2. actor的回调函数拥有属性channel:Channel, 既可以发送数据又可以接收数据, produce的属性channel属于SendChannel
  3. 无论是produce或者actor他们的通道都属于Channel, 既可以发送又可以接收数据, 只需要类型强转即可
  4. 本身Channel可以进行双向数据通信, 但是设计produce和actor属于设计思想中的生产者和消费者模式
  5. 他们都属于协程作用域和数据通道的结合

 

Flow

Kotlin 协程中使用挂起函数可以实现非阻塞地执行任务并将结果返回回来,但是只能返回单个计算结果。但是如果希望有多个计算结果返回回来,则可以使用 Flow。

Flow 的简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private fun createFlow(): Flow<Int> = flow {
delay(1000)
emit(1)
delay(1000)
emit(2)
delay(1000)
emit(3)
}

fun main() = runBlocking {
createFlow().collect {
println(it)
}
}

上述代码使用 flow{ ... } 来构建一个 Flow 类型,具有如下特点:

  • flow{ ... } 内部可以调用 suspend 函数;
  • createFlow 不需要 suspend 来标记;(为什么没有标记为挂起函数,去可以调用挂起函数?)
  • 使用 emit() 方法来发送数据;
  • 使用 collect() 方法来收集结果。

flow的发送函数emit不是线程安全的不允许其他线程调用, 如果需要线程安全请使用channelFlow而不是flow

channelFlow使用send函数发送数据

 

创建常规 Flow 的常用方式:

flow{…}

1
2
3
4
5
6
7
8
flow {
delay(1000)
emit(1)
delay(1000)
emit(2)
delay(1000)
emit(3)
}

flowOf()

1
2
3
flowOf(1,2,3).onEach {
delay(1000)
}

flowOf() 构建器定义了一个发射固定值集的流, 使用 flowOf 构建 Flow 不需要显示调用 emit() 发射数据

asFlow()

1
2
3
listOf(1, 2, 3).asFlow().onEach {
delay(1000)
}

使用 asFlow() 扩展函数,可以将各种集合与序列转换为流,也不需要显示调用 emit() 发射数据.

集合或者Sequence都可以通过asFlow函数转成Flow对象

Channel通道转成Flow

1
public fun <T> ReceiveChannel<T>.consumeAsFlow(): Flow<T> 

甚至挂起函数也可以转成Flow

1
public fun <T> (suspend () -> T).asFlow(): Flow<T>

 

Flow 是冷流(惰性的)

在调用末端流操作符(collect 是其中之一)之前, flow{ … } 中的代码不会执行。我们称之为冷流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private fun createFlow(): Flow<Int> = flow {
println("flow started")
delay(1000)
emit(1)
delay(1000)
emit(2)
delay(1000)
emit(3)
}

fun main() = runBlocking {
val flow = createFlow()
println("calling collect...")
flow.collect {
println(it)
}
println("calling collect again...")
flow.collect {
println(it)
}
}

结果如下:

1
2
3
4
5
6
7
8
9
10
calling collect...
flow started
1
2
3
calling collect again...
flow started
1
2
3

这是一个返回一个 Flow 的函数 createFlow 没有标记 suspend 的原因,即便它内部调用了 suspend 函数,但是调用 createFlow 会立即返回,且不会进行任何等待。而再每次收集结果的时候,才会启动流。

那么有没有热流呢? 后面讲的 ChannelFlow 就是热流。只有上游产生了数据,就会立即发射给下游的收集者。

 

Flow 的取消

流采用了与协程同样的协助取消。流的收集可以在当流在一个可取消的挂起函数(例如 delay)中挂起的时候取消。取消Flow 只需要取消它所在的协程即可。

以下示例展示了当 withTimeoutOrNull 块中代码在运行的时候流是如何在超时的情况下取消并停止执行其代码的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun simple(): Flow<Int> = flow { 
for (i in 1..3) {
delay(100)
println("Emitting $i")
emit(i)
}
}

fun main() = runBlocking<Unit> {
withTimeoutOrNull(250) { // 在 250 毫秒后超时
simple().collect { value -> println(value) }
}
println("Done")
}

注意,在 simple 函数中流仅发射两个数字,产生以下输出:

1
2
3
4
5
Emitting 1
1
Emitting 2
2
Done

 

Terminal flow operators 末端流操作符

末端操作符是在流上用于启动流收集的挂起函数。 collect 是最基础的末端操作符,但是还有另外一些更方便使用的末端操作符:

  • 转化为各种集合,toList/toSet/toCollection
  • 获取第一个(first)值,最后一个(last)值与确保流发射单个(single)值的操作符
  • 使用 reduce 与 fold 将流规约到单个值
  • count
  • launchIn/produceIn/broadcastIn

下面看几个常用的末端流操作符

 

转化为集合

1
2
3
public suspend fun <T> Flow<T>.toList(destination: MutableList<T> = ArrayList()): List<T>
public suspend fun <T> Flow<T>.toSet(destination: MutableSet<T> = LinkedHashSet()): Set<T>
public suspend fun <T, C : MutableCollection<in T>> Flow<T>.toCollection(destination: C): C。

 

reduce

reduce 类似于 Kotlin 集合中的 reduce 函数,能够对集合进行计算操作。前面提到,reduce 是一个末端流操作符。

1
2
3
4
5
6
fun main() = runBlocking {
val sum = (1..5).asFlow().reduce { a, b ->
a + b
}
println(sum)
}

输出结果:

1
15

 

fold

fold 也类似于 Kotlin 集合中的 fold,需要设置一个初始值,fold 也是一个末端流操作符。

1
2
3
4
5
6
fun main() = runBlocking {
val sum = (1..5).asFlow().fold(100) { a, b ->
a + b
}
println(sum)
}

输出结果:

1
115

 

launchIn

launchIn 用来在指定的 CoroutineScope 内启动 flow, 需要传入一个参数: CoroutineScope

源码:

1
2
3
public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch {
collect() // tail-call
}

示例:

1
2
3
4
5
6
7
private val mDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
fun main() {
val scope = CoroutineScope(mDispatcher)
(1..5).asFlow().onEach { println(it) }
.onCompletion { mDispatcher.close() }
.launchIn(scope)
}

输出结果:

1
2
3
4
5
1
2
3
4
5

再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() = runBlocking{
val cosTime = measureTimeMillis {
(1..5).asFlow()
.onEach { delay(100) }
.flowOn(Dispatchers.IO)
.collect { println(it) }

flowOf("one", "two", "three", "four", "five")
.onEach { delay(200) }
.flowOn(Dispatchers.IO)
.collect { println(it) }
}
println("cosTime: $cosTime")
}

我们希望并行执行两个 Flow ,看下输出结果:

1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
one
two
three
four
five
cosTime: 1645

结果并不是并行执行的,这个很好理解,因为第一个 collect 不执行完,不会走到第二个。

正确地写法应该是,为每个 Flow 单独起一个协程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() = runBlocking<Unit>{
launch {
(1..5).asFlow()
.onEach { delay(100) }
.flowOn(Dispatchers.IO)
.collect { println(it) }
}
launch {
flowOf("one", "two", "three", "four", "five")
.onEach { delay(200) }
.flowOn(Dispatchers.IO)
.collect { println(it) }
}
}

或者使用 launchIn, 写法更优雅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun main() = runBlocking<Unit>{

(1..5).asFlow()
.onEach { delay(100) }
.flowOn(Dispatchers.IO)
.onEach { println(it) }
.launchIn(this)

flowOf("one", "two", "three", "four", "five")
.onEach { delay(200) }
.flowOn(Dispatchers.IO)
.onEach { println(it) }
.launchIn(this)
}

输出结果:

1
2
3
4
5
6
7
8
9
10
1
one
2
3
4
two
5
three
four
five

 

中间转换操作符

map

map 操作符用于 List 表示将 List 中的每个元素转换成新的元素,并添加到一个新的 List 中,最后再讲新的 List 返回,map 操作符用于 Flow 表示将流中的每个元素进行转换后再发射出来。

1
2
3
4
5
6
fun main() = runBlocking {
(1..5).asFlow().map { "string: $it" }
.collect {
println(it)
}
}

输出:

1
2
3
4
5
string: 1
string: 2
string: 3
string: 4
string: 5

 

transform

在使用 transform 操作符时,可以任意多次调用 emit ,这是 transform 跟 map 最大的区别(map不允许)

1
2
3
4
5
6
7
8
9
fun main() = runBlocking {
(1..5).asFlow().transform {
emit(it * 2)
delay(100)
emit("String: $it")
}.collect {
println(it)
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
2
String: 1
4
String: 2
6
String: 3
8
String: 4
10
String: 5

 

onEach

遍历

1
2
3
4
5
fun main() = runBlocking {
(1..5).asFlow()
.onEach { println("onEach: $it") }
.collect { println(it) }
}

输出:

1
2
3
4
5
6
7
8
9
10
onEach: 1
1
onEach: 2
2
onEach: 3
3
onEach: 4
4
onEach: 5
5

 

filter

按条件过滤

1
2
3
4
5
fun main() = runBlocking {
(1..5).asFlow()
.filter { it % 2 == 0 }
.collect { println(it) }
}

输出结果:

1
2
2
4

 

drop / dropWhile

drop 过滤掉前几个元素

1
2
3
4
5
6
7
fun main() = runBlocking {
(1..5).asFlow().drop(2)
.collect { println(it) }
}
//3
//4
//5

dropWhile 过滤满足条件的元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Returns a flow containing all elements except first elements that satisfy the given predicate.
*/
@ExperimentalCoroutinesApi
public fun <T> Flow<T>.dropWhile(predicate: suspend (T) -> Boolean): Flow<T> = flow {
var matched = false
collect { value ->
if (matched) {
emit(value)
} else if (!predicate(value)) {
matched = true
emit(value)
}
}
}

 

take

take 操作符只取前几个 emit 发射的值

1
2
3
4
5
fun main() = runBlocking {
(1..5).asFlow().take(2).collect {
println(it)
}
}

输出:

1
2
1
2

 

zip

zip 是可以将2个 flow 进行合并的操作符

1
2
3
4
5
6
7
8
fun main() = runBlocking {
val flowA = (1..6).asFlow()
val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200)
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect {
println(it)
}
}

输出结果:

1
2
3
4
5
1 and one
2 and two
3 and three
4 and four
5 and five

zip 操作符会把 flowA 中的一个 item 和 flowB 中对应的一个 item 进行合并。即使 flowB 中的每一个 item 都使用了 delay() 函数,在合并过程中也会等待 delay() 执行完后再进行合并。

如果 flowA 和 flowB 中 item 个数不一致,则合并后新的 flow item 个数,等于较小的 item 个数

1
2
3
4
5
6
7
8
fun main() = runBlocking {
val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three","four","five", "six", "seven").onEach { delay(200) }
flowA.zip(flowB) { a, b -> "$a and $b" }
.collect {
println(it)
}
}

输出结果:

1
2
3
4
5
1 and one
2 and two
3 and three
4 and four
5 and five

 

combine

combine 也是合并,但是跟 zip 不太一样。

使用 combine 合并时,每次从 flowA 发出新的 item ,会将其与 flowB 的最新的 item 合并

1
2
3
4
5
6
7
8
fun main() = runBlocking {
val flowA = (1..5).asFlow().onEach { delay(100) }
val flowB = flowOf("one", "two", "three","four","five", "six", "seven").onEach { delay(200) }
flowA.combine(flowB) { a, b -> "$a and $b" }
.collect {
println(it)
}
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
1 and one
2 and one
3 and one
3 and two
4 and two
5 and two
5 and three
5 and four
5 and five
5 and six
5 and seven

 

flattenContactflattenMerge 扁平化处理

flattenContact
flattenConcat 将给定流按顺序展平为单个流,而不交错嵌套流。
源码:

1
2
3
4
@FlowPreview
public fun <T> Flow<Flow<T>>.flattenConcat(): Flow<T> = flow {
collect { value -> emitAll(value) }
}

例子:

1
2
3
4
5
6
7
8
fun main() = runBlocking {
val flowA = (1..5).asFlow()
val flowB = flowOf("one", "two", "three","four","five").onEach { delay(1000) }

flowOf(flowA,flowB)
.flattenConcat()
.collect{ println(it) }
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
2
3
4
5
// delay 1000ms
one
// delay 1000ms
two
// delay 1000ms
three
// delay 1000ms
four
// delay 1000ms
five

flattenMerge
fattenMerge 有一个参数,并发限制,默认位 16。

源码:

1
2
3
4
5
@FlowPreview
public fun <T> Flow<Flow<T>>.flattenMerge(concurrency: Int = DEFAULT_CONCURRENCY): Flow<T> {
require(concurrency > 0) { "Expected positive concurrency level, but had $concurrency" }
return if (concurrency == 1) flattenConcat() else ChannelFlowMerge(this, concurrency)
}

可见,参数必须大于0, 并且参数为 1 时,与 flattenConcat 一致。

1
2
3
4
5
6
7
8
fun main() = runBlocking {
val flowA = (1..5).asFlow().onEach { delay(100) }
val flowB = flowOf("one", "two", "three","four","five").onEach { delay(200) }

flowOf(flowA,flowB)
.flattenMerge(2)
.collect{ println(it) }
}

输出结果:

1
2
3
4
5
6
7
8
9
10
1
one
2
3
two
4
5
three
four
five

 

flatMapMergeflatMapContact

flatMapMerge 由 map、flattenMerge 操作符实现

1
2
3
4
5
@FlowPreview
public fun <T, R> Flow<T>.flatMapMerge(
concurrency: Int = DEFAULT_CONCURRENCY,
transform: suspend (value: T) -> Flow<R>
): Flow<R> = map(transform).flattenMerge(concurrency)

例子:

1
2
3
4
5
6
7
8
9
10
fun main() = runBlocking {
(1..5).asFlow()
.flatMapMerge {
flow {
emit(it)
delay(1000)
emit("string: $it")
}
}.collect { println(it) }
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
1
2
3
4
5
// delay 1000ms
string: 1
string: 2
string: 3
string: 4
string: 5

flatMapContact 由 map、flattenConcat 操作符实现

1
2
3
@FlowPreview
public fun <T, R> Flow<T>.flatMapConcat(transform: suspend (value: T) -> Flow<R>): Flow<R> =
map(transform).flattenConcat()

例子:

1
2
3
4
5
6
7
8
9
10
fun main() = runBlocking {
(1..5).asFlow()
.flatMapConcat {
flow {
emit(it)
delay(1000)
emit("string: $it")
}
}.collect { println(it) }
}

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1
// delay 1000ms
string: 1
2
// delay 1000ms
string: 2
3
// delay 1000ms
string: 3
4
// delay 1000ms
string: 4
5
// delay 1000ms
string: 5

flatMapMerge 和 flatMapContact 都是将一个 flow 转换成另一个流。
区别在于: flatMapMerge 不会等待内部的 flow 完成 , 而调用 flatMapConcat 后,collect 函数在收集新值之前会等待 flatMapConcat 内部的 flow 完成。

 

flatMapLatest

当发射了新值之后,上个 flow 就会被取消。

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() = runBlocking {
(1..5).asFlow().onEach { delay(100) }
.flatMapLatest {
flow {
println("begin flatMapLatest $it")
delay(200)
emit("string: $it")
println("end flatMapLatest $it")
}
}.collect {
println(it)
}
}

输出结果:

1
2
3
4
5
6
7
begin flatMapLatest 1
begin flatMapLatest 2
begin flatMapLatest 3
begin flatMapLatest 4
begin flatMapLatest 5
end flatMapLatest 5
string: 5

 

生命周期

onStart 流启动时

Flow 启动开始执行时的回调,在耗时操作时可以用来做 loading。

1
2
3
4
5
6
fun main() = runBlocking {
(1..5).asFlow()
.onEach { delay(200) }
.onStart { println("onStart") }
.collect { println(it) }
}

输出结果:

1
2
3
4
5
6
onStart
1
2
3
4
5

 

onCompletion 流完成时

Flow 完成时(正常或出现异常时),如果需要执行一个操作,它可以通过两种方式完成:

使用 try … finally 实现
1
2
3
4
5
6
7
8
9
10
11
12
fun main() = runBlocking {
try {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.collect { println(it) }
} finally {
println("Done")
}
}
通过 onCompletion 函数实现
1
2
3
4
5
6
7
8
9
fun main() = runBlocking {
flow {
for (i in 1..5) {
delay(100)
emit(i)
}
}.onCompletion { println("Done") }
.collect { println(it) }
}

输出:

1
2
3
4
5
6
1
2
3
4
5
Done

 

Flow 异常处理

catch 操作符捕获上游异常

前面提到的 onCompletion 用来Flow是否收集完成,即使是遇到异常也会执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() = runBlocking {
(1..5).asFlow().onEach {
if (it == 4) {
throw Exception("test exception")
}
delay(100)
println("produce data: $it")
}.onCompletion {
println("onCompletion")
}.collect {
println("collect: $it")
}
}

输出:

1
2
3
4
5
6
7
8
9
produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
onCompletion
Exception in thread "main" java.lang.Exception: test exception
...

其实在 onCompletion 中是可以判断是否有异常的, onCompletion(action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit) 是有一个参数的,如果flow 的上游出现异常,这个参数不为 null,如果上游未出现异常,则为 null, 据此,我们可以在 onCompletion 中判断异常。但是, onCompletion 只能判断是否出现了异常,并不能捕获异常。捕获异常可以使用 catch 操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fun main() = runBlocking {
(1..5).asFlow().onEach {
if (it == 4) {
throw Exception("test exception")
}
delay(100)
println("produce data: $it")
}.onCompletion { cause ->
if (cause != null) {
println("flow completed exception")
} else {
println("onCompletion")
}
}.catch { ex ->
println("catch exception: ${ex.message}")
}.collect {
println("collect: $it")
}
}

输出结果:

1
2
3
4
5
6
7
8
produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
flow completed exception
catch exception: test exception
  • catch 操作符用于实现异常透明化处理, catch 只是中间操作符不能捕获下游的异常,。
  • catch 操作符内,可以使用 throw 再次抛出异常、可以使用 emit() 转换为发射值、可以用于打印或者其他业务逻辑的处理等等

 

retry/retryWhen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public fun <T> Flow<T>.retry(
retries: Long = Long.MAX_VALUE, // 重试次数
predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T>

public fun <T> Flow<T>.retryWhen(
predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean
): Flow<T>
//如:
fun main() = runBlocking {
(1..5).asFlow().onEach {
if (it == 4) {
throw Exception("test exception")
}
delay(100)
println("produce data: $it")
}.retryWhen { cause, attempt ->
cause.message == "test exception" && attempt < 2
}.catch { ex ->
println("catch exception: ${ex.message}")
}.collect {
println("collect: $it")
}
}

 

Backpressure背压

Backpressure是响应式编程的功能之一, Rxjava 中的 Flowable 支持的 Backpressure策略有:

  • MISSING:创建的 Flowable 没有指定背压策略,不会对通过 OnNext 发射的数据做缓存或丢弃处理。
  • ERROR:如果放入 Flowable 的异步缓存池中的数据超限了,则会抛出 MissingBackpressureException 异常。
  • BUFFER:Flowable 的异步缓存池同 Observable 的一样,没有固定大小,可以无限制添加数据,不会抛出 MissingBackpressureException 异常,但会导致 OOM。
  • DROP:如果 Flowable 的异步缓存池满了,会丢掉将要放入缓存池中的数据。
  • LATEST:如果缓存池满了,会丢掉将要放入缓存池中的数据。这一点跟 DROP 策略一样,不同的是,不管缓存池的状态如何,LATEST 策略会将最后一条数据强行放入缓存池中。

而在Flow代码块中,每当有一个处理结果 我们就可以收到,但如果处理结果也是耗时操作。就有可能出现,发送的数据太多了,处理不及时的情况。

Flow 的 Backpressure是通过 suspend 函数实现的。

 

buffer 缓冲

buffer 对应 Rxjava 的 BUFFER 策略。 buffer 操作指的是设置缓冲区。当然缓冲区有大小,如果溢出了会有不同的处理策略。

  • 设置缓冲区,如果溢出了,则将当前协程挂起,直到有消费了缓冲区中的数据。
  • 设置缓冲区,如果溢出了,丢弃最新的数据。
  • 设置缓冲区,如果溢出了,丢弃最老的数据。

缓冲区的大小可以设置为 0,也就是不需要缓冲区。

看一个未设置缓冲区的示例,假设每产生一个数据然后发射出去,要耗时 100ms ,每次处理一个数据需要耗时 700ms,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
fun main() = runBlocking {
val cosTime = measureTimeMillis {
(1..5).asFlow().onEach {
delay(100)
println("produce data: $it")
}.collect {
delay(700)
println("collect: $it")
}
}
println("cosTime: $cosTime")
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
produce data: 1
collect: 1
produce data: 2
collect: 2
produce data: 3
collect: 3
produce data: 4
collect: 4
produce data: 5
collect: 5
cosTime: 4069

由于流是惰性的,且是连续的,所以整个流中的数据处理完成大约需要 4000ms

下面,我们使用 buffer() 设置一个缓冲区。buffer(),接收两个参数,第一个参数是 size, 表示缓冲区的大小。第二个参数是 BufferOverflow, 表示缓冲区溢出之后的处理策略,其值为下面的枚举类型,默认是 BufferOverflow.SUSPEND

处理策略源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum class BufferOverflow {
/**
* Suspend on buffer overflow.
*/
SUSPEND,

/**
* Drop **the oldest** value in the buffer on overflow, add the new value to the buffer, do not suspend.
*/
DROP_OLDEST,

/**
* Drop **the latest** value that is being added to the buffer right now on buffer overflow
* (so that buffer contents stay the same), do not suspend.
*/
DROP_LATEST
}

设置缓冲区,并采用挂起的策略

修改,代码,我们设置缓冲区大小为 2 :

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() = runBlocking {
val cosTime = measureTimeMillis {
(1..5).asFlow().onEach {
delay(100)
println("produce data: $it")
}.buffer(2)
.collect {
delay(700)
println("collect: $it")
}
}
println("cosTime: $cosTime")
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
produce data: 1
produce data: 2
produce data: 3
produce data: 4
collect: 1
produce data: 5
collect: 2
collect: 3
collect: 4
collect: 5
cosTime: 3713

可见整体用时大约为 3713ms。buffer 操作符可以使发射和收集的代码并发运行,从而提高效率。

下面简单分析一下执行流程:

这里要注意的是,buffer 的容量是从 0 开始计算的

首先,我们收集第一个数据,产生第一个数据,然后 2、3、4 存储在了缓冲区。第5个数据发射时,缓冲区满了,会挂起。等到第1个数据收集完成之后,再发射第5个数据。

设置缓冲区,丢弃最新的数据

如果上述代码处理缓存溢出策略为 BufferOverflow.DROP_LATEST,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() = runBlocking {
val cosTime = measureTimeMillis {
(1..5).asFlow().onEach {
delay(100)
println("produce data: $it")
}.buffer(2, BufferOverflow.DROP_LATEST)
.collect {
delay(700)
println("collect: $it")
}
}
println("cosTime: $cosTime")
}

输出如下:

1
2
3
4
5
6
7
8
9
produce data: 1
produce data: 2
produce data: 3
produce data: 4
produce data: 5
collect: 1
collect: 2
collect: 3
cosTime: 2272

可以看到,第4个数据和第5个数据因为缓冲区满了直接被丢弃了,不会被收集。

设置缓冲区,丢弃旧的数据

如果上述代码处理缓存溢出策略为 BufferOverflow.DROP_OLDEST,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() = runBlocking {
val cosTime = measureTimeMillis {
(1..5).asFlow().onEach {
delay(100)
println("produce data: $it")
}.buffer(2, BufferOverflow.DROP_OLDEST)
.collect {
delay(700)
println("collect: $it")
}
}
println("cosTime: $cosTime")
}

输出结果如下:

1
2
3
4
5
6
7
8
9
produce data: 1
produce data: 2
produce data: 3
produce data: 4
produce data: 5
collect: 1
collect: 4
collect: 5
cosTime: 2289

可以看到,第4个数据进入缓冲区时,会把第2个数据丢弃掉,第5个数据进入缓冲区时,会把第3个数据丢弃掉。

 

conflate 合并

当流代表部分操作结果或操作状态更新时,可能没有必要处理每个值,而是只处理最新的那个。conflate 操作符可以用于跳过中间值:

1
2
3
4
5
6
7
8
9
10
11
12
13
fun main() = runBlocking {
val cosTime = measureTimeMillis {
(1..5).asFlow().onEach {
delay(100)
println("produce data: $it")
}.conflate()
.collect {
delay(700)
println("collect: $it")
}
}
println("cosTime: $cosTime")
}

输出结果:

1
2
3
4
5
6
7
8
produce data: 1
produce data: 2
produce data: 3
produce data: 4
produce data: 5
collect: 1
collect: 5
cosTime: 1596

conflate 操作符是不设缓冲区,也就是缓冲区大小为 0,丢弃旧数据,也就是采取 DROP_OLDEST 策略,其实等价于 buffer(0, BufferOverflow.DROP_OLDEST)

 

Flow 线程切换

Flow 是基于 CoroutineContext 进行线程切换的。因为 Collect 是一个 suspend 函数,必须在 CoroutineScope 中执行,所以响应线程是由 CoroutineContext 决定的。比如,在 Main 线程总执行 collect, 那么响应线程就是 Dispatchers.Main

flowOn 切换线程

Rxjava 通过 subscribeOnObserveOn 来决定发射数据和观察者的线程。并且,上游多次调用 subscribeOn 只会以最后一次为准。

而 Flows 通过 flowOn 方法来切换线程,多次调用,都会影响到它上游的代码。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private val mDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

fun main() = runBlocking {
(1..5).asFlow().onEach {
printWithThreadInfo("produce data: $it")
}.flowOn(Dispatchers.IO)
.map {
printWithThreadInfo("$it to String")
"String: $it"
}.flowOn(mDispatcher)
.onCompletion {
mDispatcher.close()
}
.collect {
printWithThreadInfo("collect: $it")
}
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 1
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 2
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 3
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 4
thread id: 13, thread name: DefaultDispatcher-worker-1 ---> produce data: 5
thread id: 12, thread name: pool-1-thread-1 ---> 1 to String
thread id: 12, thread name: pool-1-thread-1 ---> 2 to String
thread id: 1, thread name: main ---> collect: String: 1
thread id: 12, thread name: pool-1-thread-1 ---> 3 to String
thread id: 1, thread name: main ---> collect: String: 2
thread id: 12, thread name: pool-1-thread-1 ---> 4 to String
thread id: 1, thread name: main ---> collect: String: 3
thread id: 12, thread name: pool-1-thread-1 ---> 5 to String
thread id: 1, thread name: main ---> collect: String: 4
thread id: 1, thread name: main ---> collect: String: 5

可以看到,发射数据是在 Dispatchers.IO 线程执行的, map 操作时在我们自定义的线程池中进行的,collect 操作在 Dispatchers.Main 线程进行。

 

StateFlowSharedFlow

请确保导入了依赖implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'

StateFlowSharedFlow 是用来替代 BroadcastChannel 的新的 API。用于上游发射数据,能同时被多个订阅者收集数据。

StateFlow

官方文档解释:StateFlow是一个状态容器式可观察数据流,可以向其收集器发出当前状态更新和新状态更新。还可通过其 value 属性读取当前状态值。如需更新状态并将其发送到数据流,请为 MutableStateFlow 类的 value 属性分配一个新值。

在 Android 中,StateFlow非常适合需要让可变状态保持可观察的类。

StateFlow有两种类型: StateFlowMutableStateFlow :

1
2
3
4
5
6
7
8
public interface StateFlow<out T> : `SharedFlow`<T> {
public val value: T
}

public interface MutableStateFlow<out T>: StateFlow<T>, Mutable`SharedFlow`<T> {
public override var value: T
public fun compareAndSet(expect: T, update: T): Boolean
}

状态由其值表示。任何对值的更新都会反馈新值到所有流的接收器中。

 

StateFlow基本使用

使用示例:

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
class Test {
private val _state = MutableStateFlow<String>("unKnown")
val state: StateFlow<String> get() = _state

fun getApi(scope: CoroutineScope) {
scope.launch {
val res = getApi()
_state.value = res
}
}

private suspend fun getApi() = withContext(Dispatchers.IO) {
delay(2000) // 模拟耗时请求
"hello, stateFlow"
}
}

fun main() = runBlocking<Unit> {
val test: Test = Test()

test.getApi(this) // 开始获取结果

launch(Dispatchers.IO) {
test.state.collect {
printWithThreadInfo(it)
}
}
launch(Dispatchers.IO) {
test.state.collect {
printWithThreadInfo(it)
}
}
}

结果输出如下,并且程序是没有停下来的。

1
2
3
4
5
thread id: 14, thread name: DefaultDispatcher-worker-3 ---> unKnown
thread id: 12, thread name: DefaultDispatcher-worker-1 ---> unKnown
// 等待两秒
thread id: 14, thread name: DefaultDispatcher-worker-3 ---> hello, stateFlow
thread id: 12, thread name: DefaultDispatcher-worker-1 ---> hello, stateFlow

StateFlow的使用方式与 LiveData 类似。

MutableStateFlow是可变类型的,即可以改变 value 的值。 StateFlow则是只读的。这与 LiveDataMutableLiveData一样。为了程序的封装性。一般对外暴露不可变的只读变量。

输出结果证明:

  1. StateFlow是发射的数据可以被在不同的协程中,被多个接受者同时收集的。
  2. StateFlow是热流,只要数据发生变化,就会发射数据。

程序没有停下来,因为在 StateFlow的收集者调用 collect 会挂起当前协程,而且永远不会结束。

StateFlowLiveData 的不同之处:

  1. StateFlow必须有初始值,LiveData 不需要。
  2. LiveData 会与 Activity 声明周期绑定,当 View 进入 STOPED 状态时, LiveData.observer() 会自动取消注册,而从 StateFlow或任意其他数据流收集数据的操作并不会停止。

 

为什么使用 StateFlow

我们知道 LiveData 有如下特点:

  1. 只能在主线程更新数据,即使在子线程通过 postValue()方法,最终也是将值 post 到主线程调用的 setValue()
  2. LiveData 是不防抖的
  3. LiveDatatransformation 是在主线程工作
  4. LiveData 需要正确处理 “粘性事件” 问题。

鉴于此,使用 StateFlow可以轻松解决上述场景。

 

防止任务泄漏

使用代码手动追踪一千个协程的确是很困难的。你可以尝试去追踪它们,并且手动保证它们最后会完成或者取消,但是这样的代码冗余,而且容易出错。如果你的代码不够完美,你将失去对一个协程的追踪,我把它称之为任务泄露。Flow也是如此。

手动取消 StateFlow的订阅者的协程,在 Android 中,可以从 Lifecycle.repeatOnLifecycle 块收集数据流。

对应代码如下:

1
2
3
4
5
6
7
lifecycleSope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
test.state.collect {
printWithThreadInfo(it)
}
}
}
SateFlow 只会发射最新的数据给订阅者。

我们修改上面代码:

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
class Test {
private val _state = MutableStateFlow<String>("unKnown")
val state: StateFlow<String> get() = _state

fun getApi1(scope: CoroutineScope) {
scope.launch {
delay(2000)
_state.value = "hello,coroutine"
}
}

fun getApi2(scope: CoroutineScope) {
scope.launch {
delay(2000)
_state.value = "hello, kotlin"
}
}
}

fun main() = runBlocking<Unit> {
val test: Test = Test()

test.getApi1(this) // 开始获取结果
delay(1000)
test.getApi2(this) // 开始获取结果

val job1 = launch(Dispatchers.IO) {
delay(8000)
test.state.collect {
printWithThreadInfo(it)
}
}
val job2 = launch(Dispatchers.IO) {
delay(8000)
test.state.collect {
printWithThreadInfo(it)
}
}

// 避免任务泄漏,手动取消
delay(10000)
job1.cancel()
job2.cancel()
}

现在的场景是,先请求 getApi1(), 一秒之后再次请求 getApi2(), 这样 StateFlow的值加上初始值,一共被赋值过 3 次。确保,三次赋值都完成后,我们再收集 StateFlow中的数据。
输出结果如下:

1
2
thread id: 13, thread name: DefaultDispatcher-worker-2 ---> hello, kotlin
thread id: 12, thread name: DefaultDispatcher-worker-1 ---> hello, kotlin

结果显示了,StateFlow只会将最新的数据发射给订阅者。对比 LiveData, LiveData 内部有 version 的概念,对于注册的订阅者,会根据 version 进行判断,将历史数据发送给订阅者。即所谓的“粘性”。我不认为 “粘性” 是 LiveData 的设计缺陷,我认为这是一种特性,有很多场景确实需要用到这种特性。StateFlow则没有此特性。

那总不能需要用到这种特性的时候,我又使用 LiveData 吧?下面要说的 SharedFlow 就是用来解决此种场景的。

 

SharedFlow

如果只是需要管理一系列状态更新(即事件流),而非管理当前状态.则可以使用 SharedFlow 共享流。如果对发出的一连串值感兴趣,则这API十分方便。相比 LiveData 的版本控制,SharedFlow 则更灵活、更强大。

SharedFlow 也有两种类型:SharedFlowMutableSharedFlow

1
2
3
4
5
6
7
8
9
10
public interface `SharedFlow`<out T> : Flow<T> {
public val replayCache: List<T>
}

interface Mutable`SharedFlow`<T> : `SharedFlow`<T>, FlowCollector<T> {
suspend fun emit(value: T)
fun tryEmit(value: T): Boolean
val subscriptionCount: StateFlow<Int>
fun resetReplayCache()
}

SharedFlow 是一个流,其中包含可用作原子快照的 replayCache。每个新的订阅者会先从replay cache中获取值,然后才收到新发出的值。

MutableSharedFlow可用于从挂起或非挂起的上下文中发射值。顾名思义,可以重置 MutableSharedFlowreplayCache。而且还将订阅者的数量作为 Flow 暴露出来。

实现自定义的 MutableSharedFlow 可能很麻烦。因此,官方提供了一些使用 SharedFlow 的便捷方法:

1
2
3
4
5
public fun <T> Mutable`SharedFlow`(
replay: Int, // 当新的订阅者Collect时,发送几个已经发送过的数据给它
extraBufferCapacity: Int = 0, // 减去replay,Mutable`SharedFlow`还缓存多少数据
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND // 缓存溢出时的处理策略,三种 丢掉最新值、丢掉最旧值和挂起
): Mutable`SharedFlow`<T>

MutableSharedFlow 的参数解释在上面对应的注释中。

 

SharedFlow 基本使用
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
class `SharedFlow`Test {
private val _state = Mutable`SharedFlow`<Int>(replay = 3, extraBufferCapacity = 2)
val state: `SharedFlow`<Int> get() = _state

fun getApi(scope: CoroutineScope) {
scope.launch {
for (i in 0..20) {
delay(200)
_state.emit(i)
println("send data: $i")
}
}
}
}

fun main() = runBlocking<Unit> {
val test: `SharedFlow`Test = `SharedFlow`Test()

test.getApi(this) // 开始获取结果

val job = launch(Dispatchers.IO) {
delay(3000)
test.state.collect {
println("---collect1: $it")
}
}
delay(5000)
job.cancel() // 取消任务, 避免泄漏
}

输出结果如下:

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
send data: 0
send data: 1
send data: 2
send data: 3
send data: 4
send data: 5
send data: 6
send data: 7
send data: 8
send data: 9
send data: 10
send data: 11
send data: 12
send data: 13
---collect1: 11
---collect1: 12
---collect1: 13
send data: 14
---collect1: 14
send data: 15
---collect1: 15
send data: 16
---collect1: 16
send data: 17
---collect1: 17
send data: 18
---collect1: 18
send data: 19
---collect1: 19
send data: 20
---collect1: 20

分析一下该结果:
SharedFlow 每 200ms 发射一次数据,总共发射 21 个数据出来,耗时大约 4s。
SharedFlow 的 replay 设置为 3, extraBufferCapacity 设置为2, 即 SharedFlow 的缓存为 5 。缓存溢出的处理策略是默认挂起的。
订阅者是在 3s 之后开始手机数据的。此时应该已经发射了 14 个数据,即 0-13, SharedFlow 的缓存为 8, 缓存的数据为 9-13, 但是,只给订阅者发送 3 个旧数据,即订阅者收集到的值是从 11 开始的。

 

MutableSharedFlow 的其它接口

MutableSharedFlow 还具有 subscriptionCount 属性,其中包含处于活跃状态的收集器的数量,以便相应地优化业务逻辑。
MutableSharedFlow 还包含一个 resetReplayCache 函数,在不想重放已向数据流发送的最新信息的情况下使用。

 

安卓中使用协程

依赖:implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4

 

Android中使用协程的一些最佳做法

注入调度器

在创建新协程或调用 withContext 时,请勿对 Dispatchers 进行硬编码。

1
2
3
4
5
6
7
8
9
10
11
12
// DO inject Dispatchers
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

// DO NOT hardcode Dispatchers
class NewsRepository {
// DO NOT use Dispatchers.Default directly, inject it instead
suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

这种依赖项注入模式可以降低测试难度,因为您可以使用 TestCoroutineDispatcher 替换单元测试和插桩测试中的这些调度程序,以提高测试的确定性。

 

挂起函数应该保证线程安全

挂起函数应该保证对任意线程安全,不应该由挂起函数的调用方来切换线程。

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
class NewsRepository(private val ioDispatcher: CoroutineDispatcher) {

// As this operation is manually retrieving the news from the server
// using a blocking HttpURLConnection, it needs to move the execution
// to an IO dispatcher to make it main-safe
suspend fun fetchLatestNews(): List<Article> {
withContext(ioDispatcher) { /* ... implementation ... */ }
}
}

// This use case fetches the latest news and the associated author.
class GetLatestNewsWithAuthorsUseCase(
private val newsRepository: NewsRepository,
private val authorsRepository: AuthorsRepository
) {
// This method doesn't need to worry about moving the execution of the
// coroutine to a different thread as newsRepository is main-safe.
// The work done in the coroutine is lightweight as it only creates
// a list and add elements to it
suspend operator fun invoke(): List<ArticleWithAuthor> {
val news = newsRepository.fetchLatestNews()

val response: List<ArticleWithAuthor> = mutableEmptyList()
for (article in news) {
val author = authorsRepository.getAuthor(article.author)
response.add(ArticleWithAuthor(article, author))
}
return Result.Success(response)
}
}

此模式可以提高应用的可伸缩性,因为调用挂起函数的类无需担心使用哪个 Dispatcher 来处理哪种类型的工作。该责任将由执行相关工作的类承担。

 

ViewModel 应创建协程

ViewModel 类应首选创建协程,而不是公开挂起函数来执行业务逻辑。如果只需要发出一个值,而不是使用数据流公开状态,ViewModel 中的挂起函数就会非常有用。

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
// DO create coroutines in the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {

private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
val uiState: StateFlow<LatestNewsUiState> = _uiState

fun loadNews() {
viewModelScope.launch {
val latestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
}
}
}

// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
// DO NOT do this. News would probably need to be refreshed as well.
// Instead of exposing a single value with a suspend function, news should
// be exposed using a stream of data as in the code snippet above.
suspend fun loadNews() = getLatestNewsWithAuthors()
}

视图不应直接触发任何协程来执行业务逻辑,而应将这项工作委托给 ViewModel。这样一来,业务逻辑就会变得更易于测试,因为可以对 ViewModel 对象进行单元测试,而不必使用测试视图所必需的插桩测试。

此外,如果工作是在 viewModelScope 中启动,您的协程将在配置更改后自动保留。如果您改用 lifecycleScope 创建协程,则必须手动进行处理该操作。如果协程的存在时间需要比 ViewModel 的作用域更长,请查看“在业务和数据层中创建协程”部分。

直白点理解就是业务逻辑,应该在 ViewModel 中启动协程处理,而不是在 View 中。

注意:视图应对与界面相关的逻辑启动协程。例如,从互联网提取映像或设置字符串格式。

 

安卓Kotlin开发常见知识

SAM 转换

您可以通过实现 OnClickListener 接口来监听 Android 中的点击事件。Button 对象包含一个 setOnClickListener() 函数,该函数接受 OnClickListener 的实现。

OnClickListener 具有单一抽象方法 onClick(),您必须实现该方法。因为 setOnClickListener() 始终接受 OnClickListener 作为参数,又因为 OnClickListener 始终都有相同的单一抽象方法,所以此实现在 Kotlin 中可以使用匿名函数来表示。此过程称为单一抽象方法转换,简称 SAM 转换。

例如,有这样一个 Kotlin 函数式接口:

1
2
3
fun interface IntPredicate {
fun accept(i: Int): Boolean
}

如果不使用 SAM 转换,那么你需要像这样编写代码:

1
2
3
4
5
6
// 创建一个类的实例
val isEven = object : IntPredicate {
override fun accept(i: Int): Boolean {
return i % 2 == 0
}
}

通过利用 Kotlin 的 SAM 转换,可以改为以下等效代码:

1
2
// 通过 lambda 表达式创建一个实例
val isEven = IntPredicate { it % 2 == 0 }

可以通过更短的 lambda 表达式替换所有不必要的代码。

1
2
3
4
5
6
7
8
9
fun interface IntPredicate {
fun accept(i: Int): Boolean
}

val isEven = IntPredicate { it % 2 == 0 }

fun main() {
println("Is 7 even? - ${isEven.accept(7)}")
}

SAM 转换可使代码明显变得更简洁。以下示例展示了如何使用 SAM 转换来为 Button 实现 OnClickListener

1
2
3
4
5
6
7
8
9
10
11
loginButton.setOnClickListener {
val authSuccessful: Boolean = viewModel.authenticate(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
if (authSuccessful) {
// Navigate to next screen
} else {
statusTextView.text = requireContext().getString(R.string.auth_failed)
}
}

当用户点击 loginButton 时,系统会执行传递给 setOnClickListener() 的匿名函数中的代码。

 

参考

https://book.kotlincn.net/

https://www.cnblogs.com/joy99/p/15805916.html

https://juejin.cn/post/6844904037586829320