Android Kotlin

Kotlin笔记(基础)

2022-06-30  本文已影响0人  NoBugException

1、main函数

fun main() {
    println("Helo Kotlin")
}

2、常量和变量

val:常量
var:变量

在 Android 项目中,常量还可以这样写:

 companion object {
    const val hello: String = "HELLO"
}

3、Math函数

kotlin.math.max(a, b)

4、大括号可以省略

当函数体只有一句代码时,大括号可以省略

fun print() = println("Helo Kotlin")
fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)
fun largerNumber(num1: Int, num2: Int) = max(num1, num2) 

5、if 可以有返回值

fun largerNumber(num1: Int, num2: Int): Int {
    var value = 0
    if (num1 > num2) {
        value = num1
    } else {
        value = num2
    }
    return value
} 

if 有返回值,可以写成:

fun largerNumber(num1: Int, num2: Int): Int {
    val value = if (num1 > num2) {
        num1
    } else {
        num2
    }
    return value
}

可以直接 return

fun largerNumber(num1: Int, num2: Int): Int {
    return if (num1 > num2) {
        num1
    } else {
        num2
    }
}

去除大括号

fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) {
    num1
} else {
    num2
}

当 if 里面只有一句代码时,可以省略大括号

fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2

6、when语句

当 if...else... 太多时,可以考虑使用 when 语句,相当于 Java 的 switch 语句

fun getScore(name: String) = when (name) {
    "Tom" -> 86
    "Jim" -> 87
    "Jack" -> 88
    "Lily" -> 89
    else -> 0
}

when还可以检查数据类型,结合 is 使用

fun checkNumber(num: Number) {
    when (num) {
        is Int -> println("number is Int")
        is Double -> println("number is Double")
        else -> println("number not support")
    }
}

is 相当于 Java 的 instanceof

if(num is Int) {}

7、比较字符串是否相等

直接使用 ==

8、区间的表示

闭区间:

val range = 0..10
val range : IntRange = 0..10

左闭右开:

val range = 0 until 10 

升序区间:.. 和 until 
降序区间:downTo

9、for 循环

for (i in 0..10) { // 闭区间
    println(i)
}

for (i in 0 until 10) { // 左开右闭
    println(i)
}

for (i in 0 until 10 step 2) { // step 相当于 i = i + 2
    println(i)
}

10、继承

Kotlin和Java不同,Kotlin任何一个非抽象类默认不可以继承,添加 open 关键字可以让非抽象类继承。

open class Person { 
 ... 
} 

class Student : Person() {
    var sno = ""
    var grade = 0
}

不带参数的构造方法:
class Student() : Person() {
    var sno = ""
    var grade = 0
}

带参数的构造方法:
class Student(val sno : String, val grade : Int) : Person() {

}

var s = Student("zhangsan", 5)
println(s.sno)
println(s.grade)

这样的话,主构造函数没有实现体,如果想要在主构造函数中实现一些逻辑:


class Student(val sno : String, val grade : Int) : Person() {
    init {
        println("sno is " + sno)
        println("grade is " + grade)
    }
}

次构造函数是通过constructor关键字来定义的

constructor(name: String, age : Int) : this("", 0,name, age)
constructor() : this("", 0)

this 直接或间接调用主构造函数。

11、方法修饰符

public、private、protected和internal
默认修饰符是 public
internal:只对同一模块下可见

12、数据类

使用 data 关键字,声明该类为数据类

data class Cellphone(val brand: String, val price: Double) 

Kotlin会根据主构造函数中的参数帮你将equals()、hashCode()、toString()等
且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。

13、单例

object A {
    fun print(){
        print("test")
    }
}

单例的调用方法:

A.print()

14、集合

 val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") // 不可变的集合
 val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape") // 可变集合

val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape") // 不可变的集合
val set = mutableSetOf("Apple", "Banana", "Orange", "Pear", "Grape") // 可变集合

val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5); // 不可变的集合
val map = mutableMapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5); // 不可变的集合

集合的遍历:

for (fruit in list) {
    println(fruit)
}

for (fruit in set) {
    println(fruit)
}

for ((fruit, number) in map) {
    println(fruit + "--" + number)
}

// map将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda 表达式中指定,最终生成一个新的集合
val newlist = list.map { it.uppercase() }
val newlist = list.map { it.substring(0 until 1) }
val newlist = list.map { it.length }

// filter 滤集合中的数据
val newlist = list.filter { it.length > 5 }.map { it.uppercase() }

// 集合中是否存在4个字母以内的单词
val anyResult = list.any { it.length <= 4 }
// 集合中是否所有单词都在10个字母以内
val allResult = list.all { it.length <= 10 }

15、Java函数式API的使用

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}).start();


首先将Java代码转换成Kotlin:

Thread(object : Runnable {
    override fun run() {
        System.out.println("Thread is running")
    }
}).start()

Runnable是一个接口,并且只有一个方法需要实现,所以可以使用Java函数式API:

Thread(Runnable {
    System.out.println("Thread is running")
}).start()

由于Thread方法只有一个参数,且 Runnable 是抽象方法接口,那么可以省略接口名:

Thread({ System.out.println("Thread is running") }).start()

 当Lambda表达式是方法的最后一个参数时,可以移到方法的外面:

Thread(){ System.out.println("Thread is running") }.start()

同时,Lambda表达式还是方法的唯一参数,所以小括号可以省略:

Thread { println("Thread is running") }.start()


button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    }
});

将以上代码转成Kotlin格式:

button.setOnClickListener {
        
}

16、判空

fun doStudy(study: Study) { 
    study.readBooks() 
    study.doHomework() 
}

Kotlin默认所有的参数和变量都不可为空,所以这里传入的Study参数也一定不会为空。
Kotlin将空指针异常的检查提前到了编译时期。

那如果我们的业务逻辑就是需要某个参数或者变量为空该怎么办呢?

fun doStudy(study: Study) {
    study.readBooks()
    study.doHomework()
}

study 默认不可以为空,但是如果在 Study 后面添加  ? 呢?

fun doStudy(study: Study?) {

}

添加 ? 之后,表明 study 可以为空,如下图所示:
image.png
由于 study 可能为空,所以报错,这时需要处理 study 为空的所有可能才不会报错,这样就避免了 Kotlin 空指针异常的可能。

修改后的代码如下:

fun doStudy(study: Study?) {
    if (study == null){
        return
    }
    study.readBooks()
    study.doHomework()
}

if 语句可能会增加代码的复杂度,可以改成如下代码:

fun doStudy(study: Study?) {
    study?.readBooks()
    study?.doHomework()
}

当 study 为空时,则什么都不做,当 study 不为空时,则调用。

// 当 a 不为空,则返回a,否则返回b
val c = a ?: b 

// 这是一种有风险的写法,意在告诉Kotlin,我非常确信这里的对象不会为空,
// 所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。
val upperCase = content!!.toUpperCase() 

let 和 ?.结合使用:

fun doStudy(study: Study?) {
    study?.let { stu ->
        stu.readBooks()
        stu.doHomework()
    }
}

fun doStudy(study: Study?) {
    study?.let {
        it.readBooks()
        it.doHomework()
    }
}

此外,let函数是可以处理全局变量的判空问题的,而 if 判断语句则无法做到这一点。

17、字符串内嵌表达式

val hello = "hello, ${name}. nice to meet you!";
val hello = "hello, $name. nice to meet you!";

18、参数默认值

fun printParams(num: Int, str: String = "hello") {
    println("num is $num , str is $str")
}

printParams(11)
printParams(11, "zhangsan")

fun printParams(num: Int = 1, str: String) {
    println("num is $num , str is $str")
}

printParams(str = "zhangsan")
printParams(11, str = "zhangsan")
printParams(num = 11, str = "zhangsan")

19、Kotlin扩展

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'org.jetbrains.kotlin.android.extensions'
}

加入扩展:id 'org.jetbrains.kotlin.android.extensions'
可以省去 findViewById 写法。

val fragment = supportFragmentManager.findFragmentById(R.id.leftFrag) as LeftFragment
val fragment = leftFrag as LeftFragment

20、Activity跳转

val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)

SecondActivity::class.java 相当于 Java 的 SecondActivity.class

21、标准函数

在 Standard.kt 文件中,定义了一些标准函数

常见的标准函数有 let、with、run和apply

let 前面已经说过了,主要是和 `?.` 结合使用;

with 有两个参数,第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda 表达式。

原有代码:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val builder = StringBuilder()
builder.append("Start eating fruits.\n")
for (fruit in list) {
    builder.append(fruit).append("\n")
}
builder.append("Ate all fruits.")
val result = builder.toString()
println(result)

改进后的代码:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = with(StringBuilder()){
    // 闭包中的上下文就是 StringBuilder
    append("Start eating fruits.\n")
    for (fruit in list) {
        append(fruit).append("\n")
    }
    append("Ate all fruits.")
    toString()
}
println(result)

run 和 with 功能上差不多,只是语法存在差异:

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().run{
    // 闭包中的上下文就是 StringBuilder
    append("Start eating fruits.\n")
    for (fruit in list) {
        append(fruit).append("\n")
    }
    append("Ate all fruits.")
    toString()
}
println(result)

apply 语法和 run 一样,apply 无法指定返回值类型,它的返回值只能是对象本身

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val result = StringBuilder().apply{
    // 闭包中的上下文就是 StringBuilder
    append("Start eating fruits.\n")
    for (fruit in list) {
        append(fruit).append("\n")
    }
    append("Ate all fruits.")
}
println(result.toString())

22、静态方法

Kotlin 中就非常推荐使用单例类的方式来实现

object Util { 
    fun doAction() { 
         println("do action") 
    } 
}

不过,使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,
而如果我们只是希望让类中的某一个方法变成静态方法的调用方式该怎么办呢?
使用 companion object 可以解决这个问题

class Util {

    fun doAction1() {
        println("do action1")
    }

    // doAction2()方法其实也并不是静态方法,companion object这个关键字实际上会 在Util类的内部创建一个伴生类
    companion object {
        fun doAction2() {
            println("do action2")
        }
    }
}

val u = Util();
u.doAction1()
Util.doAction2()

不管是单例方法还是伴生类方法,Kotlin 编译器并不会将那些方法编译成静态方法,
但是,如果在方法前面添加注解 @JvmStatic 之后,Kotlin 编译器就会编译成静态方法。

companion object {
    @JvmStatic
    fun doAction2() {
        println("do action2")
    }
}

还有一种方式是:顶层方法
顶层方法:不在类中的方法,在Kotlin项目的任何地方都可以调用顶层方法;
Kotlin 编译器会将所有的顶层方法全部编译成静态方法;

假设在 Test.kt 文件中添加 doSomething 方法,在 kt 文件的任何位置直接调用即可:

    doSomething()

如果是Java文件,调用方式如下:

    TestKt.doSomething();

23、多次执行

    repeat(2) {

    }

24、延迟初始化

private lateinit var textView: TextView

当你对一个全局变量使用了lateinit关键字时,请一定要确保它在被任何地方调用之前已经完成了初始化工作,
否则Kotlin 将无法保证程序的安全性。

判断是否已经初始化
if (::textView.isInitialized)

25、将lettersCount()函数添加到String类当中(扩展函数)

fun String.lettersCount(str: String): Int {
    var count = 0
    for (char in str) {
        if (char.isLetter()) {
            count++
        }
    }
    return count
}

将 lettersCount 方法添加到String中。

val count = "ABC123xyz!@#".lettersCount()

26、运算符重载

class Money(val value : Int) {

    operator fun plus(money: Money) = Money(money.value + value)

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

operator fun plus 是重载+的固定写法,不能改变

val money = Money(10) + Money(20)
val money2 = Money(10) + 1
println(money.value)
println(money2.value)

以上代码是重载+的实现方法,剩下的参考下表即可:
1656427756044.png
如果是非自定义类,比如SDK自带的String,那么可以结合扩展函数用法,将重载运算符的方法添加到类中:

operator fun String.times(n: Int): String {
    val builder = StringBuilder()
    repeat(n) {
        builder.append(this)
    }
    return builder.toString()
}

val str= "abc" * 3
println("str:$str")

另外,必须说明的是,其实Kotlin 的String类中已经提供了一个用于将字符串重复n遍的repeat()函数,
因此times()函数还可以进一步精简成如下形式:

operator fun String.times(n: Int) = repeat(n)

27、高阶函数

定义:如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。

除了基本数据类型之外,Kotlin 还增加了`函数类型`

`函数类型`的基本规则如下:

(String, Int) -> Unit

->左边的部分就是用来声明该函数接收什么参数的,多个参数之间使用逗号隔开,如果不接收任何参数,写一对空括号就可以了
->右边的部分用于声明该函数的返回值是什么类型,如果没有返回值就使用Unit,它大致相当于Java 中的 void

如果将上述函数类型添加到某个函数的参数声明或者返回值声明上,那么这个函数就是一个高阶函数。

举例1:

fun example(func: (String, Int) -> Unit) {
    func("hello", 123)
}

fun func(str : String, num : Int) {
    println("$str $num")
}

example(::func)



举例2:

fun plus(num1: Int, num2: Int): Int {
    return num1 + num2
}
fun minus(num1: Int, num2: Int): Int {
    return num1 - num2
}

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int = operation(num1, num2)

val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2, ::plus)
val result2 = num1AndNum2(num1, num2, ::minus)
println("result1 is $result1")
println("result2 is $result2")

以上代码可以使用Lambda表达式简化,防止定义多个方法:

举例1(简化):

fun example(func: (String, Int) -> Unit) {
    func("hello", 123)
}

example { str, num -> println("$str $num") }


举例2(简化):

fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int = operation(num1, num2)

val num1 = 100
val num2 = 80
val result1 = num1AndNum2(num1, num2) { num1, num2 ->
    num1 + num2
}
val result2 = num1AndNum2(num1, num2) { num1, num2 ->
    num1 - num2
}
println("result1 is $result1")
println("result2 is $result2")

28、内联函数

Lambda 表达式在底层被转换成了匿名类的实现方式。
我们每调用一次Lambda 表达式,都会创建一个新的匿名类实例,当会造成额外的内存和性能开销。

为了解决这个问题,Kotlin 提供了内联函数的功能,它可以将使用Lambda 表达式带来的运行时开销完全消除。


内联函数的用法非常简单,只需要在定义高阶函数时加上inline关键字的声明即可:

inline fun example(func: (String, Int) -> Unit) = func("hello", 123)

inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int = operation(num1, num2)

Kotlin 编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。

如果我们只想内联其中的一个Lambda 表达式该怎么办呢?这时就可以使用 `noinline` 关键字了:

inline fun inlineTest(block1: () -> Unit, noinline block2: () -> Unit) {}

内联函数和非内联函数的区别?
【1】内联函数没有真正的参数属性,非内联的函数类型参数可以自由地传递给其他任何函数,
     因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,
     这也是它最大的局限性;
【2】是内联函数所引用的Lambda 表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回;

返回和局部返回?
【1】内联函数:支持 return@printString 和 return;
【2】非内联函数:仅支持 return@printString

如果在高阶函数中创建了另外的Lambda 或者匿名类的实现,并且在这些实现中调用函数类型参数,
此时再将高阶函数声明成内联函数,编译器一定会提示错误。

如下代码一定会报错:

inline fun runRunnable(lock: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

使用 crossinline关键字可以解决该问题,crossinline 关键字可以解决 return 冲突的问题:

inline fun runRunnable(crossinline block: () -> Unit) {
    val runnable = Runnable {
        block()
    }
    runnable.run()
}

声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda 表达式中使用return关键字进行函数返回了,
但是仍然可以使用return@runRunnable的写法进行局部返回。
总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他所有特性。

29、?、!!、?:、::、===

"?"加在变量名后,系统在任何情况不会报它的空指针异常。
"!!"加在变量名后,如果对象为null,那么系统一定会报异常。
"?:"对象A ?: 对象B 表达式,意思为,当对象 A值为 null 时,那么它就会返回后面的对象 B
"::"把一个方法当做一个参数,传递到另一个方法中进行使用,通俗的来讲就是引用一个方法
在Kotlin中,=== 表示比较对象地址,== 表示比较两个值大小

30、泛型

泛型类:

class MyClass<T> {
    fun method(param: T): T {
        return param
    }
}

val myClass = MyClass<Int>()
val result = myClass.method(123)


泛型方法:

class MyClass {
    fun <T> method(param: T): T {
        return param
    }
}

val myClass = MyClass()
val result = myClass.method<Int>(123)

对泛型的类型进行约束:

class MyClass<T : Number> {
    fun  method(param: T): T {
        return param
    }
}

class MyClass {
    fun <T : Number> method(param: T): T {
        return param
    }
}

泛型在高级函数中的应用:

fun StringBuilder.build(block: StringBuilder.() -> Unit): StringBuilder {
    block()
    return this
}

StringBuilder.() 中的 StringBuilder 指定了具体的上下文类型。

sb.build {
    append("A")
    append("B")
    append("C")
}

以上代码闭包中的上下文就是 StringBuilder,那么,结合泛型,闭包中的上下文类型是可以改成泛型的:

fun <T> T.build(block: T.() -> Unit): T {
    block()
    return this
}

21、懒加载

private val uriMatcher by lazy {
    val matcher = UriMatcher(UriMatcher.NO_MATCH)
    matcher.addURI(authority, "book", bookDir)
    matcher.addURI(authority, "book/#", bookItem)
    matcher.addURI(authority, "category", categoryDir)
    matcher.addURI(authority, "category/#", categoryItem)
    matcher
}

by lazy 代码块是Kotlin 提供的一种懒加载技术,代码块中的代码一开始并不会执行,
只有当uriMatcher变量首次被调用的时候才会执行,
并且会将代码块中最后一行代码的返回值赋给uriMatcher。

22、类型转换

使用 as 进行类型转换,但是 as 是不安全的,有可能会因为不能成功转换而发生crash,代码如下:

val y=null
val x:String = y as String

解决方案是:使用安全的类型转换 as?

val y=null
val x: String? = y as? String

23、委托

类委托:于将一个类的具体实现委托给另一个类去完成(相当于Java的装饰者设计模式)

class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
    override val size: Int = helperSet.size
    override fun contains(element: T) = helperSet.contains(element)
    override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
    override fun isEmpty() = helperSet.isEmpty()
    override fun iterator() = helperSet.iterator()
}

val set = HashSet<String>()
set.add("A")
set.add("B")
val mySet = MySet(set)
println("size:${mySet.size}")

MySet 类中定义的方法基本都是 HashSet 已有的方法,为了避免多个方法的声明,可以使用 by 关键字:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet{
}

以上两段代码的行为是一致的。

接下来,我们就可以在 MySet 类中重写已有方法,或者新增其他实现。

委托属性:

var p by Delegate() // 将p属性的具体实现委托给了Delegate类去完成


下面使用委托属性,实现懒加载:

class Later<T>(val block : () -> T) {

    var value : Any? = null

    /**
     * 对象第一次被调用时执行
     */
    operator fun getValue(any: Any?, property: KProperty<*>): T {
        println("getValue")
        if (value == null) {
            value = block()
        }
        return value as T
    }

    /**
     * 被复制或对象属性发生变化时执行
     */
    operator fun setValue(any1: Any?, property: KProperty<*>, any2: Any) {
        println("setValue")
    }

}

var p by Later<String> {
    println("p 执行了")
    "A"
}
p.toString()

24、infix函数

目的:增加代码的可读性

新建 infix.kt,代码如下:

package com.example.myapplication

infix fun String.beginsWith(prefix: String) = startsWith(prefix)

infix fun <T> Collection<T>.has(element: T) = contains(element)

使用如下:

if ("Hello Kotlin" beginsWith "Hello") {
    // 处理具体的逻辑
}

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "Banana") {
    // 处理具体的逻辑
}

25、泛型的高级特性

【1】泛型实化

inline  + reified 可以将泛型实化

// 泛型实化
inline fun <reified T> getGenericType() = T::class.java

val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
println("result1 is $result1")
println("result2 is $result2")

Java语言不支持泛型实化,所以 T.class 必然会报错,Kotlin 将 T 实化后,`T::class.java` 不会报错。

可以新建 reified.kt 文件,将泛型实化代码放进去。

启动 Activity 封装:

inline fun <reified T> startActivity(context: Context, block: Intent.() -> Unit) {
    val intent = Intent(context, T::class.java)
    intent.block()
    context.startActivity(intent)
}

startActivity<MainActivity>(this) {
    putExtra("name", "zhangsan")
    putExtra("age", 14)
}

【2】泛型的协变和逆变(不常用)

泛型的协变:假如定义了一个MyClass<T>的泛型类,其中 A 是 B 的子类型,
同时 MyClass<A> 又是 MyClass<B> 的子类型,那么我们就可以称MyClass在 T 这个泛型上是协变的。

out:方法返回值位置
in:方法参数位置

如何才能让MyClass<A>成为MyClass<B>的子类型呢?

class SimpleData<out T>(val data : T) {
    fun get(): T? {
        return data
    }
}

out 将泛型只能用于输出位置。

这样,在使用的时候就不会报错:

fun main() {
    val student = Student("Tom", 19)
    val data = SimpleData<Student>(student)
    handleSimpleData(data)
    val studentData = data.get()
}

fun handleSimpleData(data: SimpleData<Person>) {
    val personData = data.get()
}

泛型的逆变:假如定义了一个 MyClass<T> 的泛型类,其中 A 是 B 的子类型,
同时 MyClass<B>又是 MyClass<A> 的子类型, 那么我们就可以称 MyClass 在 T 这个泛型上是逆变的。

package com.example.myapplication

interface Transformer<in T> {
    fun transform(t: T): String
}

fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(t: Person): String {
            return "${t.name} ${t.age}"
        }
    }
    handleTransformer(trans)
}
fun handleTransformer(trans: Transformer<Student>) {
    val student = Student("Tom", 19)
    val result = trans.transform(student)
}

26、协程

协程:可以简单地将它理解成一种轻量级的线程

协程允许我们在单线程模式下模拟多线程编程的效果,
代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。

这种特性使得高并发程序的运行效率得到了极大的提升。

依赖:

    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"


使用 `GlobalScope.launch` 创建一个顶层协程

fun main() {
    // 创建一个协程作用域
    GlobalScope.launch {
        println("start")
        // delay是一个非阻塞式的函数,让协程挂起,不影响其它协程运行
        delay(1500)
        println("end")
    }
    Thread.sleep(1000) // 线程阻塞
}

不会阻塞当前线程,当程序结束时,该协程会被中断。

使用 `runBlocking` 创建一个协程:

fun main() {
    
    // runBlocking 可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程
    // 通常只应该在测试环境下使用,在正式环境中使用容易产生一些性能上的问题
    runBlocking {
        println("start")
        // delay是一个非阻塞式的函数,让协程挂起,不影响其它协程运行
        delay(1500)
        println("end")
    }
}

会阻塞当前线程,直到 runBlocking 执行完毕。


使用 launch 在协程作用域中创建子协程:

// launch 必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程
// 子协程的特点是如果,外层作用域的协程结束了,该作用域下的所有子协程也会一同结束
runBlocking {
    launch {
    }

    launch {
    }
}

外部协程和两个子协程并发执行。

在携程中调用方法:

fun main() {
    runBlocking {
        printDot()
    }
}

 fun printDot() {
    println(".")
}

printDot 函数中不能使用 delay 函数来协程挂起,delay 函数必须在协程中或者挂起函数中使用。

解决这个问题需要将 printDot 函数改成挂起函数。

suspend 关键字可以将函数改成挂起函数:

 suspend fun printDot() {
    println(".")
    delay(1000)
}

如果想要在 printDot 中使用 launch 创建协程,需要使用 coroutineScope 来实现,coroutineScope  关键字只能在挂起函数或者协程中使用:

 suspend fun printDot() {
     coroutineScope {
        launch {
            println(".")
            delay(1000)
        }
     }
}

最终,优化后的代码如下:

suspend fun printDot() = coroutineScope {
    launch {
        println(".")
        delay(1000)
    }
}

coroutineScope 保证其作用域内的所有代码和子协程在全部执行完之前,外部的协程会一直被挂起。

runBlocking {
    coroutineScope {
    }
}

GlobalScope.launch和runBlocking函数是可以在任意地方调用的,
coroutineScope函数可以在协程作用域或挂起函数中调用,而launch函数只能在协程作用域中调用。
runBlocking由于会阻塞线程,因此只建议在测试环境下使用。
GlobalScope.launch由于每次创建的都是顶层协程,一般也不太建议使用。


CoroutineScope 可以将协程的作用域加到类中:

class MyClass: CoroutineScope by CoroutineScope(Dispatchers.Default){
    fun doWork() {
        launch {}
    }
    fun destroy() {
        (this as CoroutineScope).cancel()
    }
}

val myClass = MyClass();
myClass.doWork()
myClass.destroy()
myClass.launch {  }

这样 MyClass 就具备了创建协程 和 取消协程的能力了。



取消协程:

val job = GlobalScope.launch {  }
job.cancel() // 取消协程

runBlocking {
    val  job = launch {  }
    job.cancel()
}

`GlobalScope.launch` 和 `launch` 的返回值是Job,job.cancel() 可以取消协程。

在实际项目中是这样做的:

val job = Job()
val scope = CoroutineScope(job)
scope.launch {
    // 处理具体的逻辑
}
job.cancel()


如果需要返回值,则使用 async 即可,async 只能在协程中使用:

// async 方法会将当前协程阻塞住,直到返回结果为止
runBlocking {
    val result = async {
        5 + 5
    }.await()
    println(result)
}

多个 async 串行执行:

runBlocking {
    val start = System.currentTimeMillis()
    val result1 = async {
        delay(1000)
        5 + 5
    }.await()
    val result2 = async {
        delay(1000)
        4 + 6
    }.await()
    println("result is ${result1 + result2}.")
    val end = System.currentTimeMillis()
    println("cost ${end - start} ms.")
}

总耗时:2048 ms

多个 async 并行执行:

runBlocking {
    val start = System.currentTimeMillis()
    val deferred1 = async {
        delay(1000)
        5 + 5
    }
    val deferred2 = async {
        delay(1000)
        4 + 6
    }
    val res1 = deferred1.await()
    val res2 = deferred2.await()
    println("result is ${res1 + res2}.")
    val end = System.currentTimeMillis()
    println("cost ${end - start} ms.")
}

总耗时:1032 ms

async 的 start 参数:

async 是可以带参数的:

async(start = CoroutineStart.DEFAULT) : 默认参数,async 代码块即可执行
async(start = CoroutineStart.LAZY) : 在这种情况下,协程会在两种场景下去执行:调用Deferred的await方法,或是调用Job的start方法
async(start = CoroutineStart.ATOMIC)    
async(start = CoroutineStart.UNDISPATCHED)

withContext() 函数是一个挂起函数,大体可以将它理解成 async 函数的一种简化版写法

runBlocking {
    val result = withContext(Dispatchers.Default) {
        5 + 5
    }
    println(result)
}

Dispatchers.Default 为线程参数,它的可选值为 Dispatchers.Default、Dispatchers.IO 和 Dispatchers.Main。

Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,
开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。

Dispatchers.IO表示会使用一种较高并发的线程策略,当你要执行的代码大多数时间是在阻塞和等待中,
比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。

Dispatchers.Main则表示不会开启子线程,而是在Android 主线程中执行代码,
但是这个值只能在Android 项目中使用。

withContext 必须指定在哪个线程执行,而 runBlocking 既可以指定某线程,也可以不指定,如果不指定就默认在当前线程执行:

runBlocking {

}

也可以指定线程:

runBlocking(Dispatchers.IO) {

}

协程超时:

    runBlocking {
    try {
        val reslt1 = withTimeout(500) {
            delay(500)
            "hello1"
        }
        println(reslt1)
    } catch (e: TimeoutCancellationException) {
        println("timeout")
    }

    val reslt2 = withTimeoutOrNull(500) {
        delay(500)
        "hello2"
    }
    println(reslt2)
}

如何使用协程优化回调方法?

使用 suspendCoroutine 函数可以优化回调方法,有几个重要的细节:
【1】suspendCoroutine 函数必须在协程作用域或挂起函数中才能调用
【2】主要作用是将当前协程立即挂起
【3】在一个普通的线程中执行Lambda 表达式中的代码

演示代码如下:

suspend fun request(address: String): String {
    return suspendCoroutine { continuation ->
        sendHttpRequest(address, object : HttpCallbackListener {
            override fun onFinish(response: String) {
                continuation.resume(response)
            }
            override fun onError(e: Exception) {
                continuation.resumeWithException(e)
            }
        })
    }
}

fun sendHttpRequest(address: String, callbackListener: HttpCallbackListener) {
    when(address) {
        "A" -> callbackListener.onFinish("finish")
        "B" -> callbackListener.onError(RuntimeException("error"))
    }
}

fun main() {
    runBlocking {
        try {
            val response = request("B")
            println(response)
        } catch (e: Exception) {
            println(e.message)
        }
    }
}

线程切换(切换上下文):

newSingleThreadContext("Ctx1").use { ctx1 ->
    newSingleThreadContext("Ctx2").use { ctx2 ->
        runBlocking(ctx1) {
            withContext(ctx2) {}
        }
    }
}

简单总结:
// 1、runBlocking:阻塞主线程
// 2、GlobalScope.launch:不阻塞主线程
// 3、在协程中使用 launch 新建多个子协程(同一线程池),launch 和 launch之间是并发执行
// 4、挂起函数关键字:suspend
// 5、coroutineScope{}必须在挂起函数或者协程里使用,直接外部协程会被挂起,coroutineScope{}执行完之后才会执行外部协程
// 6、CoroutineScope 可以将协程的作用域加到类中,class MyClass: CoroutineScope by CoroutineScope(Dispatchers.Default)
// 7、取消协程的方法:
//    val job = GlobalScope.launch {  }
//    job.cancel() // 取消协程
//
//    runBlocking {
//        val  job = launch {  }
//        job.cancel()
//    }
//
//    val job = Job()
//    val scope = CoroutineScope(job)
//    scope.launch {
//        // 处理具体的逻辑
//    }
//    job.cancel()
// 8、async 方法会将当前协程阻塞住,直到返回结果为止
//    多个async并发执行:
//    val result1 = async {}.await()
//    val result2 = async {}.await()
//    多个async并行执行:
//    val deferred1 = async {}
//    val deferred2 = async {}
//    val res1 = deferred1.await()
//    val res2 = deferred2.await()
// 9、withContext async的简化版
//    val result = withContext(Dispatchers.Default) {}
//    withContext 具有超时机制:
//       withTimeout(500):超时抛出TimeoutCancellationException异常
//       withTimeoutOrNull(500):超时返回null
// 10、suspendCoroutine 让回调方法有返回值
//     val response = request("B")
//     suspend fun request(address: String): String {
//         return suspendCoroutine { continuation ->
//             sendHttpRequest(address, object : HttpCallbackListener {
//                 override fun onFinish(response: String) {
//                     continuation.resume(response) // 释放
//                 }
//                 override fun onError(e: Exception) {
//                     continuation.resumeWithException(e) // 释放
//                 }
//             })
//         }
//     }
//    fun sendHttpRequest(address: String, callbackListener: HttpCallbackListener) {
//        when(address) {
//            "A" -> callbackListener.onFinish("finish")
//            "B" -> callbackListener.onError(RuntimeException("error"))
//        }
//    }
// 11、Android项目中常用的写法
// val coroutineScope = CoroutineScope(Dispatchers.Main)
// coroutineScope.launch { } : 返回值是 Job,Job可以取消协程
// coroutineScope.async { }:返回值是 Deferred<T>
// val str: String = coroutineScope.async { "" }.await():有返回值
// 如果想要有返回值,我们常用 withContext 代替 async :
// val str: String = withContext(coroutineScope.coroutineContext) { "" }
// 12、结合 ViewModel 使用
// 如果某个类继承ViewModel或AndroidViewModel,那么自带viewModelScope
// 直接使用:
//         viewModelScope.launch(Dispatchers.IO) {
//          }
// viewModelScope 在 ViewModel 的作用域内执行,
// 如果 ViewModel 因用户离开屏幕而被销毁,则 viewModelScope 会自动取消,且所有运行的协程也会被取消
// 13、网络请求的协程用法
// class LoginViewModel(
//    private val loginRepository: LoginRepository
// ): ViewModel() {
//
//    fun login(username: String, token: String) {
//        viewModelScope.launch {
//            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
//            val result = loginRepository.makeLoginRequest(jsonBody)
//            when (result) {
//                is Result.Success<LoginResponse> -> // Happy path
//                else -> // Show error in UI
//            }
//        }
//    }
// }
//     class LoginRepository(...) {
//        ...
//        suspend fun makeLoginRequest(
//            jsonBody: String
//        ): Result<LoginResponse> {
//
//            // Move the execution of the coroutine to the I/O dispatcher
//            return withContext(Dispatchers.IO) {
//                // Blocking network request code
//            }
//        }
//    }
// 协程可以确保网络请求避免阻塞主线程, makeLoginRequest 方法使用 suspend 修饰,suspend 将当前协程挂起,
// 等待 makeLoginRequest 执行完成则回复当前协程,并返回指定类型的返回值。类似于阻塞线程,但协程可以不阻塞主线。

27、耗时计算

// 毫秒
val time1 = measureTimeMillis {

}

// 纳秒
val time2 = measureNanoTime {

}

28、vararg

vararg关键字,它允许方法接收任意多个同等类型的参数,相当于Java的可变长参数(...)。

    fun max(vararg nums: Int): Int {
    var maxNum = Int.MIN_VALUE
    for (num in nums) {
        maxNum = kotlin.math.max(maxNum, num)
    }
    return maxNum
}

val a = 10
val b = 15
val c = 5
val d = 20
val largest = max(a, b, c, d)

以上代码是由缺陷的,就是这个方法指定了 int 类型,不能比较其他整型,所以需要进行改造:

fun <T: Comparable<T>> max(vararg nums: T): T {
    if (nums.isEmpty())
        throw RuntimeException("Params can not be empty.")
    var maxNum = nums[0]
    for (num in nums) {
        if (num > maxNum) {
            maxNum = num
        }
    }
    return maxNum
}

在 Java 中,如果想要实现所有数据类型都可以比较,必须实现 Comparable 接口, Kotlin 同样成立。

29、内部类和嵌套类

Kotlin 内部类与嵌套类的区别是:
1、内部类会带有一个外部类的对象的引用,嵌套类则没有
2、内部类需要使用 inner class 定义,而嵌套类则使用 class 定义

Kotlin 内部类会带有一个对外部类的对象的引用,所以内部类可以访问外部类成员属性和成员函数。

Kotlin 内部类使用 this@[外部类名] 持有外部类对象的引用。

30、密封类

// sealed:密封类,同时具有枚举和普通类的特性
// 密封类及其子类必须声明在同一个 kotlin 文件中
sealed class Result // 反编译得到抽象类
class Success(val code: Int) : Result()
class Exception(val code: Int, val message: String) : Result()

// 在密封类Result中,只定义了两个子类,所以在使用when语句时,不需要else
fun handleResult(result: Result): String{
    return when(result) {
        is Success -> {
            "success"
        }
        is Exception -> {
            "exception"
        }
    }
}

31、继承 Parcelable

java语言可以直接继承Parcelable,然而Kotlin需要添加插件:
id 'kotlin-android-extensions' 
或 
id 'kotlin-parcelize' 
或
id 'org.jetbrains.kotlin.android.extensions'

然后如下:

@Parcelize
class DemoEvent(val content: String) : Parcelable

[本章完...]

上一篇下一篇

猜你喜欢

热点阅读