• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

kotlin知识点

武飞扬头像
bp1907
帮助1

kotin知识点

一、kotlin基础

1.变量与函数

1.1变量

val优先使用来声明变量
var
Kotlin抛弃了java中的基本数据类型,全部使用了对象数据类型。
Number类型时Kotlin内置的一个抽象类,Int、Long、Float、Double等与数字相关的类都是它的子类

Java基本数据类型 Kotlin对象数据类型
int Int
long Long
short Short
float Float
double Double
boolean Boolean
char Char
byte Byte
1.2函数

运行代码的载体
fun methodName(param1: Int, param2: Int): Int {max(num1, num2)}

fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)
fun largerNumber(num1: Int, num2: Int) = max(num1, num2)

2.逻辑控制

2.1 if条件语句

可以有返回值,if语句每一个条件中最后一行代码的返回值

2.2 when条件语句

when() {
匹配值 -> {执行逻辑}
}
is关键字是类型匹配的核心,相当于java的instanceof
kotlin中判断字符串或对象是否相等可以直接使用==关键字

2.3 循环语句

while循环
for循环
for - in 循环
区间

val range = 0..10 //[0,10]
val range2 = 0 until 10 //[0,10)
val range3 = 0 until 10 step 2 //表示0.2.4.6.8,相当于i = i   2
val range4 = 10 downTo 1 //[10,1]

fun main() {
    for(i in 0..10) {
        println(i)
    }
}

3.面向对象编程

3.1 类与对象

Class
File通常适用于编写Kotlin顶层函数和扩展函数的。

3.2 继承与构造函数

Kotlin中任何一个非抽象类默认都是不可以被继承的,相当于java中给类声明了final关键字,在类前加上关键字open,类就可以被继承

open class Person {
    ...
}

java中继承关键字是extends,而Kotlin中是一个冒号

class Student : Person() {
    ...
}

Person类后面加一个括号和主构造函数有关。Kotlin中两种构造函数:主构造函数和次构造函数
主构造函数
每个类默认都会有一个不带参数的主构造函数,也可以显示的指明参数,主构造函数特点是没有函数体,需要写逻辑可以在init结构体中写

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

java继承中规定,子类中的构造函数必须调用父类中的构造函数,Kotlin也一样。Person类后面的一对括号表示Student类的主构造函数在初始化的时候会调用Person类的无参构造函数

class Person(val name: String, val age: Int) {
    ...
}
class Student(val sno: String, val grade: Int, name; string, age: Int) : Person(name, age) {
    init {
        println("sno is"   sno)
        println("grade is"   grade)
    }
}

注意:Student类主构造函数中增加name和age两个字段时,不能再将它们声明成val(让它们的作用域仅限定再主构造函数中),因为主构造函数中声明成val或者var的参数将自动成为该类的字段,就会导致和父类中同名的name和age字段造成冲突
次构造函数
Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数

class Student(val sno: String, val grade: Int, name; string, age: Int) : Person(name, age) {
    //调用了主构造函数
    constructor(name: String, age: Int) : this("", 0, name, age) {
    }
    //调用了第一个次构造函数,间接调用了主构造函数
    constructor() : this("", 0) {
    }
}

接下来有一个特殊情况,当一个类没有显示的定义主构造函数,定义了次构造函数

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

Student类没有主构造函数,继承Person类时也就不需要再加上括号了

3.3 接口

Kotlin中统一使用冒号表示继承类和实现接口,用逗号分隔。如果接口中的函数拥有了函数体,这个函数体中的内容就是它的默认实现。

interface Study {
    fun readBook()
    fun doHomework()
}

class Student(name: String, age:Int) : Person(name, age) , Study {
    override fun readBook() {
        println(name   " is reading.")
    }
     override fun doHomework() {
        println(name   " is doing homework.")
    }
}

Kotlin与java相比变化较大的部分——函数的可见性修饰符

修饰符 Java Kotlin
public 所有类可见 所有类可见(默认)
private 当前类可见 当前类可见
protected 当前类、子类、同一包路径下的类可见 当前类、子类可见
default 同一包路径下的类可见(默认)
internal 同一模块中的类可见
3.4 数据类与单例类

数据类使用data关键字,当一个类没有任何代码时,可以将尾部的大括号省略。

data class Student(val sno: String, val grade: Int)

常见的java单例类写法

public class Singleton {
    private static Singleton instance;

    private Singleton() {}

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

    public void singletonTest() {
        System.out.println("singletonTest is called.");
    }
}
学新通

在Kotlin中创建单例类,只需将class关键字改成object关键字即可。

object Singleton {
    fun singletonTest() {
        println("singletonTest is called.")
    }
}

//调用
Singleton.singletonTest()

调用的写法看上去像是静态方法的调用,其实Kotlin在背后自动创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例

4.Lambda编程

4.1 集合的创建与遍历

listOf()函数创建的是一个不可变集合,即只可读。可变集合使用mutableListOf()

fun main() {
    val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
    for(fruit in list) {
        println(fruit)
    }
}

Set集合的创建是setOf和mutableSetOf(),Set集合底层是使用hash映射机制来存放数据的,集合中的元素无法保证有序。

Kotlin中不建议使用put()和get()方法对Map进行添加和读取数据操作,推荐使用数组下标的语法结构,创建Map也可以收用mapOf()和mutableMapOf()

val map1 = HashMap<String, Int>()
    map1["Apple"] = 1
    map1["Banana"] = 2
    map1["Orange"] = 3

val map2 = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3)
for((fruit, number) in map2) {
        println("fruit is $fruit, number is $number")
}

这里的to并不是关键字,而是infix函数

4.2 集合的函数式API

Lambda表达式的语法结构
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
大括号,参数列表,参数列表结尾使用->符号,表示参数列表的结束以及函数体的开始,函数体可以编写任意行代码(虽然不建议编写太长的代码),并且最后一行代码会自动作为Lambda表达式的返回值。

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length})

Kotlin规定,当lambda参数是函数的最后一个参数时,可以将lambda表达式移到函数括号外面

val maxLengthFruit = list.maxBy(){ fruit: String -> fruit.length}

如果Lambda参数是函数的唯一个一个地个参数的话,可以将函数的括号省略

val maxLengthFruit = list.maxBy{ fruit: String -> fruit.length}

Kotlin的类型推导机制,参数列表大多数情况不必声明参数类型

val maxLengthFruit = list.maxBy{ fruit -> fruit.length}

当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替

val maxLengthFruit = list.maxBy{ it.length}

集合中的map函数是最常用的一种函数式API,它用于将集合中的每个元素都映射成一个另外的值,映射规则由Lambda表达式指定,最终生成新的集合

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val newList = list.map { it.toUpperCase() }

还有另外一个比较常用的函数式API,filter函数,即过滤集合中的数据

val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
val newList = list.filter { it.length < 5 }.map { it.toUpperCase() }

还有两个比较常用的API,any和all函数,any函数用于判断集合中是否至少存在一个元素满足指定条件。all函数用于判断集合中是否所有的元素都满足指定条件

4.3 Java函数式API的使用

如果我们再Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口(接口中只有一个待实现方法)参数,就可以使用函数式API。

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

由于Runnable类中只有一个待实现方法,即使没有显示地重写run()方法,Kotlin也能自动明白Runnable后面的Lambda表达式就是要在run()方法中实现的内容,如下

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

另外,如果一个Java方法的参数列表中不存在一个以上Java单抽象方法接口参数,我们还可以将接口名省略,如下

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

根据Kotlin规定还可以进一步省略

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

注册一个按钮的点击事件,OnClickListener也是单抽象方法接口

button.setOnClickListener {  }

5.空指针检查

5.1 可空类型系统

Kotlin利用编译时判空检查的机制几乎杜绝了空指针异常。

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

以上代码是没有空指针风险的,因为Kotlin默认所有参数和变量都不可为空,如果尝试向doStudy()函数传入一个null参数,则会提示错误,即Kotlin将空指针异常的检查提前到了编译时期
如果想要某个参数或者变量为空,需要在类名后面加上一个问号,比如,Int表示不可为空的整型,而Int?就表示可为空的整型

5.1 判空辅助工具
  1. ?.操作符,当对象不为空时正常调用方法,当对象为空时则什么都不做
  2. ?:操作符,左右两边都接收一个表达式,如果左边的表达式不为空就返回左边表达式的结果,否则就返回右边的结果
  3. !!非空断言,强行通过编译,在对象的后面加上!!
  4. let函数,Kotlin中的标准函数,配合?.操作符来进行辅助判空处理,可以处理全局变量的判空问题,if判断语句则无法做到这一点,用法如下
obj.let { obj2 -> 
       //编写具体的业务逻辑,obj2就是obj
    }

Tips:
字符串内嵌表达式
${}
函数的参数默认值
Kotlin提供了给函数设定参数默认值的功能,很大程度上能替代次构造函数的作用

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

二、标准函数和静态函数

1 标准函数with、run、和apply

Kotlin的标准函数指的是Standard.kt文件中定义的函数。

1.1 with函数
val result = with(obj) {
    //这里是obj的上下文
    "value"//with函数的返回值
}
1.2 run函数
val result = obj.run {
    //这里是obj的上下文
    "value"//run函数的返回值
}
1.3 apply函数
val result = obj.apply {
    //这里是obj的上下文
}
//result == obj
//intent启动activity可以使用apply

2 定义静态方法

Kotlin极度弱化了静态方法这个概念,提供了比静态方法更好用的语法特性,单例类。

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

这里的doAction()方法不是静态方法,但是可以使用Util.doAction()的方法来调用。
但是使用单例类的写法会将整个类中的所有方法全部变成类似于静态方法的调用方式,如果只希望让类中的某一个方法变成静态方法的调用方式,可以使用companion object,如下

class Util {
    fun doAction1() {
        println("do action1.")
    }

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

doAction1()方法是一定要先创建Util类的实例才能调用,而doAction2()方法可以直接使用Util.doAction2()的方式调用。
但是doAction2()方法也不是静态方法,companion object这个关键字实际上会在Util类的内部创建一个伴生类,而doAction2()方法就是定义在这个伴生类里面的实例方法,只是Kotlin会保证Util类始终只会存在一个伴生对象,因此调用Util.doAction2()方法实际是调用了Util类中伴生类对象的doAction2()方法。
如果需要定义真正的静态方法,Kotlin提供了两种实现方式:注解和顶层方法。
注解
给单例类或companion object中的方法加上@JvmStatic注解,Kotlin编译器就会将这些方法编译成真正的静态方法,如下

class Util {
    fun doAction1() {
        println("do action1.")
    }

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

此时Java和Kotlin中都可以使用Util.doAction2()的写法来调用了。
注意:@JvmStatic注解只能加在单例类或companion object中的方法上。

顶层方法
顶层方法是指没有定义在任何类中的方法。
创建一个文件,编写以下方法

fun doSomething() {
    println("do something.")
}

在Kotlin中,可以在任何位置直接调用,输入doSomething()即可。在Java代码中,创建的文件取名比如叫Helper,Kotlin编译器会自动创建一个叫HelperKt的Java类,doSomething()方法就是以静态方法的形式定义在HelperKt类里,所以在Java中使用HelperKt.doSomething()的写法来调用

三、延迟初始化和密封类

1.对变量延迟初始化

延迟初始化使用的是lateinit关键字,它可以告诉kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。

private lateinit var str: String

::adapter.isInitialized可用于判断变量是否已经初始化。

2.使用密封类优化代码

密封类帮助写出更加规范和安全的代码。

interface Result
class Success(val msg: String) : Result
class Failure(val error: Exception) : Result

fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
    else -> throw IllegalArgumentException()
}

这里不编写else条件,Kotlin编译器会认为这里缺少条件分支,代码将无法编译通过。但实际上Result的执行结果只可能是Success或者Failure。
如果此时新增一个类实现Result接口,但是没有在条件分支添加,编译器就会进入else,抛出异常,其实是没有异常的。
密封类的关键字是sealed class

sealed class Result
class Success(val msg: String) : Result()
class Failure(val error: Exception) : Result()

fun getResultMsg(result: Result) = when (result) {
    is Success -> result.msg
    is Failure -> result.error.message
}

密封类是一个可继承的类,所以在继承它的时候需要在后面加上一对括号。这里没有else条件,可以编译通过,因为当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求将每一个子类所对应的条件全部处理
注意:密封类及其所有子类只能定义在同一个文件的顶层位置

四、扩展函数和运算符重载

1.扩展函数

扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
定义扩展函数的语法结构

fun ClassName.methodName(param1: Int, param2: Int) : Int {
    return 0
}

相比于定义一个普通的函数,定义扩展函数只需要在函数名的前面加上一个ClassName.的语法结构,就表示将该函数添加到指定类当中了。
举个例子,实现统计字符串中字母的数量的功能。
那就需要在String类中添加一个扩展函数,因此需要先创建一个String.kt文件。文件名虽然没有固定的要求,但是建议向哪个类中添加扩展函数,就定义一个同名的Kotlin文件,便于以后查找。扩展函数也是可以定义在任何一个现有的类中,并不一定要创建新文件。通常来说,最好将它定义成顶层方法,这样可以让扩展函数拥有全局的访问域。
在String.kt文件中编写以下代码

fun String.lettersCount() : Int {
    var count = 0
    for (char in this) {//这里的this指字符串本身
        if(char.isLetter()) {
            count  
        }
    }
    return count
}

调用就可以这么写:

val count = "ABD6587&&%$#".lettersCount()

2.运算符重载

运算符重载是用的是operator关键字,只要在指定的函数前面加上operator关键字,就可以实现运算符重载的功能。指定的函数,加号运算符对应的是plus()函数,减号对应的minus()函数。
下面实现两个对象相加的功能

class Obj {
    operator fun plus(obj: Obj) : Obj {
        //处理相加逻辑
    }
}

让两个Money对象相加

class Money(val value: Int) {
    operator fun plus(money: Money): Money {
        val sum = value   money.value
        return Money(sum)
    }
}

调用

val money1 = Money(5)
val money2 = Money(20)
val money3 = money1   money2
println(money3.value)

由于Kotlin允许我们对同一个运算符进行多重重载,给Money加上直接和数字相加的功能,

class Money(val value: Int) {
    operator fun plus(money: Money): Money {
        val sum = value   money.value
        return Money(sum)
    }

    operator fun plus(newValue: Int): Money {
        val sum =  value   newValue
        return Money(sum)
    }
}

Kotlin允许我们重载的运算符和关键字多达十几个,下表列出常用的可重载运算符和关键字对应的语法糖表达式,以及它们会被转换成的实际调用函数。

语法糖表达式 实际调用函数
a b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a a.inc()
a– a.dec()
a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
a == b a.equals(b)
a > b a.compareTo(b)
a < b a.compareTo(b)
a >= b a.compareTo(b)
a <= b a.compareTo(b)
a…b a.rangeTo(b)
a[b] a.get(b)
a[b] = c a.set(b, c)
a in b b.contains(a)

五、高阶函数

1. 定义高阶函数

如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
函数类型的定义:
(Sting, Int) -> Unit
现在将上述函数类型添加到某个函数的参数声明或者返回值声明上,那么这个函数就是一个高阶函数

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

高阶函数允许让函数类型的参数来决定函数的执行结果。
定义一个叫做num1AndNum2()的高阶函数,并让它接收两个整型和一个函数类型的参数,在函数中对传入的两个整型参数进行某种运算,并返回最终的运算结果,但是具体进行什么运算由传入的函数类型参数决定。

fun num1AndNum2(num1: Int, num2: Int, operator: (Int, Int) -> Int) : Int {
    return operator(num1, num2)
}

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

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

接下来调用

fun main() {
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1, num2, ::plus)
    val result2 = num1AndNum2(num1, num2, ::minus)
    println("result is $result1")
    println("result2 is $result2")
}

这里::plus和::minus是一种函数引用方式的写法。但是每次调用任何高阶函数的时候都还得先定义一个与其函数类型参数相匹配的函数,过于复杂。
Kotlin还支持其他多种方式来调用高阶函数,比如Lambda表达式、匿名函数、成员引用。
将上述代码改成Lambda表达式的写法

fun main() {
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1, num2) {n1, n2 ->
        n1   n2
    }
    val result2 = num1AndNum2(num1, num2) {n1, n2 ->
        n1 - n2
    }
    println("result is $result1")
    println("result2 is $result2")
}

接下来根据之前学习的apply函数,使用高阶函数模仿实现类似的功能,给StringBuilder加一个扩展函数

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

这里函数类型参数的声明方式和之前学的语法不同,它在函数类型前面加上了一个StringBuilder.的语法结构,这是这才是定义高阶函数完整的语法规则,在函数类型前面加上ClassName.就表示这个函数类型是定义在哪个类当中的。
这里函数类型定义在StringBuilder类中,当我们调用build函数时传入的Lambda表达式将会自动拥有StringBuilder的上下文。
此时的build函数只能作用在StringBuilder类上面,而apply函数是可以作用在所有类上面的,如果想实现apply函数这个功能,需要借助Kotlin的泛型才行。

2. 内联函数的作用

fun num1AndNum2(num1: Int, num2: Int, operator: (Int, Int) -> Int) : Int {
    return operator(num1, num2)
}

fun main() {
    val num1 = 100
    val num2 = 80
    val result1 = num1AndNum2(num1, num2) {n1, n2 ->
        n1   n2
    }
}

Kotlin代码最终还是要编译成Java字节码的,但Java中并没有高阶函数的概念。
Kotlin的编译器会将这些高阶函数的语法转换成Java支持的语法结构,上述代码大致会被转换成如下Java代码

public static int num1AndNum2 (int num1, int num2, Function operation) {
    int result =(int) operation.invoke (num1, num2);
    return result;
}
public static void main() {
    int numl = 100;
    int num2 = 80;
    int result = num1AndNum2 (num1, num2, new Function() {
        @Override
        public Integer invoke (Integer n1, Integer n2) {
            return n1   n2;
        }
    });
}

可以看到第三个参数变成了一个Function接口,这是一种Kotlin内置的接口,里面有一个待实现的invoke()函数,而num1AndNum2()函数其实就是调用了Function接口的invoke()函数。
在调用num1AndNum2()函数的时候,之前的Lambda表达式在这里变成了Function接口的匿名类实现,然后在invoke()函数中实现了n1 n2的逻辑。
这就时Kotlin高阶函数背后的实现原理。我们一直调用的Lambda表达式在底层被转换成了匿名类的实现方式。即我们调用一次Lambda表达式,都会创建一个新的匿名类实例,当然也会造成额外的内存和性能开销。
为了解决这个问题,Kotlin提供了内联函数的功能,它可以将使用Lambda表达式带来的运行时开销完全消除。
内联函数的用法:在定义高阶函数时加上inline关键字的声明

inline fun num1AndNum2(num1: Int, num2: Int, operator: (Int, Int) -> Int) : Int {
    return operator(num1, num2)
}

内联函数的原理就是Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样就不存在运行时的开销了。
首先,Kotlin编译器会将Lambda表达式中的代码替换到函数类型参数调用的地方,然后再将内联函数中的全部代码替换到函数调用的地方。

3. noinline与crossinline

4. 高阶函数的应用

4.1 简化SharedPreferences的用法
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
        editor.putString("name", "Tom")
        editor.putInt("age", 28)
        editor.putBoolean("married", false)
        editor.apply()

使用高阶函数简化SharedPreferences的用法

fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
        val editor = edit()
        editor.block()
        editor.apply()
    }

首先,通过扩展函数的方式向SharedPreferences类中添加了一个open函数,并且它还接收一个函数类型的参数,因此open函数是一个高阶函数
由于open函数内拥有SharedPreferences的上下文,所以直接调用edit()方法来获取SharedPreferences.Editor对象。
另外open函数接收的是一个SharedPreferences.Editor的函数类型参数,因此这里需要调用editor.block()对函数类型参数进行调用,就可以在函数类型参数的具体实现中添加数据了。
最后还要调用editor.apply()方法来提交数据,完成数据存储操作。
项目中使用如下

getSharedPreferences("data", Context.MODE_PRIVATE).open {
            putString("name", "Tom")
            putInt("age", 28)
            putBoolean("married", false)
        }

Google提供的KTX扩展库中已经包含了上述SharedPreferences的简化用法,这个扩展库会在Android Studio创建项目的时候自动引入build.gradle的dependencies中,即implementation ‘androidx.core:core-ktx:1.0.2’
因此,在项目中可以直接使用如下方法向SharedPreferences存储数据:

getSharedPreferences("data", Context.MODE_PRIVATE).edit {
            putString("name", "Tom")
            putInt("age", 28)
            putBoolean("married", false)
        }

edit函数就是Google的KTX库中自带的。

4.2 简化ContentValues的用法
val values = ContentValues()
        values.put("name", "Game of Thrones")
        values.put("author", "Sti")
        values.put("pages", "660")
        values.put("price", "21.2")
        db.insert("Book", null, values)

KTX库中提供了contentValueOf()方法

val values = contentValuesOf("name"  to "Game of Thrones", "author" to "Sti",
        "pages" to "660", "price" to "21.2")
    db.insert("Book", null, values)

六、泛型和委托

1. 泛型的基本用法

泛型主要有两种定义方式:一种是定义泛型类,另一种是定义泛型方法,语法结构都是,T是一种约定俗成的泛型写法。
泛型类

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

MyClass就是一个泛型类,MyClass中的方法允许使用T类型的参数和返回值。
在调用MyClass类和method()方法时,就可以将泛型指定成具体的类型:

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

泛型方法

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

调用

val myClass = MyClass()
    val result = myClass.method<Int>(111)//由于Kotlin的类型推到机制,这里的Int可以省略

Kotlin还允许我们对泛型的类型进行限制。目前method()方法的泛型可以指定成任意类型,可以通过指定上界的方式来对泛型的类型进行约束,将method()方法的泛型上界设置为Number类型

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

在默认情况下,所有的泛型都是可以指定成可空类型的,因为在不手动指定上界的时候,泛型的上界默认是Any?。如果想要让泛型的类型不可为空,只需将泛型的上界手动指定成Any。
下面对之前高阶函数手写的build函数改造一下,让它可以作用在所有类上。

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

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

2. 类委托和委托属性

委托是一种设计模式,他的基本理念是:操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。Kotlin将委托功能分为两种:类委托和委托属性
类委托
核心思想是将一个类的具体实现委托给另一个类去完成。
Set是一个接口,使用它就要使用它具体的实现,比如HashSet。而借助于委托模式,我们可以轻松实现一个自己的实现类:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> {
    override val size: Int
        get() = 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()

}

MySet构造函数接收一个HashSet参数,这就相当于一个辅助对象。如果我们只是让大部分方法实现调用辅助对象中的方法,少部分的方法实现由自己来重写,甚至加入一些自己独有的方法,那么MySet就会成为一个全新的数据结构类,这就是委托模式的意义所在。
如果接口中的待实现方法很多,这种写法就会很麻烦。在Kotlin中通过类委托的功能来解决。
Kotlin中委托使用的关键字是by,只需要在接口声明的后面使用by关键字,再接上受委托的辅助对象,就可以免去之前所写的一大堆代码,如下:

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

这两段代码的效果是一模一样的,明显简化很多。如果我们要对某个方法重新实现,只需单独重写那一个方法,如下:

class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet {
    fun helloWorld() = println("hello world")
    override fun isEmpty() = false
}

这里我们新增了一个helloWorld()方法,并且重写了isEmpty()方法,其他Set接口中的功能,则和HashSet保持一致。这就是Kotlin的类委托所能实现的功能。
委托属性
核心思想是将一个属性的具体实现委托给另一个类去完成。
语法结构如下:

class MyClass {
    var p by Delegate()
}

class Delegate {
    var propValue: Any? = null
    operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? {
        return propValue
    }

    operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) {
        propValue = value
    }
}

这种写法就代表着将p属性的具体实现委托给了Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。
Dlegate类的写法是一种标准的代码实现模板,在Delegate类中我们必须实现getValue()和setValue()方法,并且都要使用operator关键字声明。
getValue()方法要接收两个参数:第一个参数用于声明该Delegate类的委托功能可以在什么类中使用;第二个参数KProperty<*>是Kotlin中的一个属性操作类,可用于获取各种属性相关的值,在当前场景用不到,但是必须在方法参数上进行声明。另外< *>这种泛型的写法表示你不知道或者不关心泛型的具体类型,只为了通过语法编译,有点类似Java中<?>的写法。至于返回值可以声明成任何类型,根据具体逻辑编写。
setValue()方法有三个参数,前两个和getValue()方法相同,最后一个参数表示具体要赋值给委托属性的值,这个参数的类型必须和getValue()方法的返回值保持一致。
整个委托属性的工作流程就是这样实现的。
不过,存在一种情况可以不用在Delegate类中实现setValue()方法,那就是MyClass中的p属性是使用val关键字声明的。

3. 实现一个自己的lazy函数

懒加载技术,把想要延迟执行的代码放到by lazy代码块中,这样代码块中的代码在一开始的时候就不会执行,只有当懒加载对象首次被调用的时候,代码块中的代码才会执行。

val p by lazy { ... }

by是Kotlin的关键字,lazy是一个高阶函数。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候,其实调用的是Delegate对象的getValue()方法,然后getValue()方法又会调用lazy函数传入的Lambda表达式,这样表达式的代码就会执行,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。
根据懒加载原理,我们实现一个自己的lazy函数,新建一个Later.kt文件,并编写以下代码

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

接下来在Later类中实现getValue()方法

class Later<T>(val block: () -> T) {
    var value: Any? = null
    operator fun getValue(any: Any?, prop: KProperty<*>): T {
        if(value == null) {
            value = block()
        }
        return value as T
    }
}

getValue()方法的第一个参数指定成Any?类型,表示Later的委托功能在所有类中都可以使用。
由于懒加载技术是不会对属性进行赋值的,所以不用实现setValue()方法。
然后定义一个顶层函数

fun <T> later(block: () -> T) = Later(block)

later懒加载函数完成。
为了方便验证,写法如下:

val p by later {
        Log.d("TAG", "run block")
        "test later"
    }

将这段代码放在一个Activity中,并在按钮的点击事件里调用属性p。
当Activity启动时,later函数中的日志不会打印,只有当首次点击按钮,日志才会打印。而再次点击按钮的时候,日志也不会再打印出来,因为代码块中的代码只会执行一次

4. 泛型的高级特性

4.1 对泛型进行实化

JDK1.5中,Java才引入泛型功能,Java的泛型功能是通过类型擦除机制来实现的。就是说泛型对于类型的约束只在编译时期存在,JVM是识别不出来我们在代码中指定的泛型类型的。例如,我们创建一个List< String>集合,虽然在编译时期只能向集合中添加字符串类型的元素,但在运行期JVM只能识别出来它是个List。
所有基于JVM的语言,它们的泛型功能都是通过类型擦除机制来实现的,当然就包括了Kotlin,这种机制使得我们不可能使用a is T或者T::class.java这样的语法,因为T的实际类型在运行的时候已经被擦除了。
不同的是,Kotlin提供了内联函数的概念,内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样就不存在泛型擦除的问题了,因为代码在编译后会直接使用实际的类型来替代内联函数中的泛型声明

fun foo() {
    bar<String>() //m
}

inline fun <T> bar() {
    //do something with T type //n
}

编译期n处代码会替换到m处,最终执行情况如下

fun foo() {
    //do something with String type
}

这就意味着,Kotlin中是可以将内联函数中的泛型进行实例化的。
将泛型实化,首先该函数必须是内联函数,其次在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。

inline fun <reified T> getGenericType() {
}

上述函数中的泛型T就是一个被实化的泛型,具体实现什么效果呢,下面实现一个获取泛型实际类型的功能

inline fun <reified T> getGenericType() = T::class.java

下面进行测试

fun main() {
    val result1 = getGenericType<String>()
    val result2 = getGenericType<Int>()
    println("result1 is $result1")
    println("result2 is $result2")
}

以上代码打印结果会是:

result1 is class java.lang.String
result2 is class java.lang.Integer
4.2 泛型实化的应用

泛型实化功能允许我们在泛型函数中获得泛型的实际类型,这就意味着类似于a is T、T::class.java这样的语法是可行的。
启动一个Activity,一般会这么写

val intent = Intent(context, TestActivity::class.java)
context.startActivity(intent)

下面改写TestActivity::class.java的写法,新建一个reified.kt文件,编写如下代码

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

Intent接收的第二个参数本来应该是一个具体的Activity的Class类型,但由于现在T已经是一个被实化的泛型了,可以直接使用T::class.java。
此时如果要启动TestActivity,可以这么写

startActivity<TestActivity>(context)

一般Intent是需要传参的,借助高阶函数就可实现。在reified.kt文件中重载startActivity()函数

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

这样在调用startActivity()函数的时候就可以在Lambda表达式中为Intent传参了

startActivity<TestActivity>(this) {
            putExtra("param1", "data")
            putExtra("param2", "111")
        }
4.3 泛型的协变

先了解一个约定,一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,我们称它为in位置,而它的返回值是输出数据的地方,我们称它为out位置

interface MyClass<T> {
    fun method(param: T): T//第一个T是in位置,第二个T是out位置
}

定义以下三个类:

open class Person(val name: String, val age: Int)
class Student(name: String, age: Int) : Person(name, age)
class Teacher(name: String, age: Int) : Person(name, age)

如果某个方法接收一个Person类型的参数,而我们传人一个Student 的实例,这是可行的。
如果某个方法接收一个List< Person>类型的参数,而我们传人一个 List< Student>的实例,在Java中是不允许这么做的,因为 List< Student>不能成为 List< Person>的子类,否则将可能存在类型转换的安全隐患,通过一个具体的例子进行说明。这里自定义个SimpleData 类,代码如下所示:

class SimpleData<T> {
    private var data: T? = null
    fun set(t: T) {
        data = t
    }
    
    fun get(): T? = data
}

假设,如果编程语言允许向某个接收SimpleData< Person>参数的方法传入SimpleData< Student>的实例:

fun main() {
    val student = Student("tom", 22)
    val data = SimpleData<Student>()
    data.set(student)
    handleSimpleData(data)//实际上这行代码会报错
    val studentData = data.get()
}

fun handleSimpleData(data: SimpleData<Person>) {
    val teacher = Teacher("jack", 33)
    data.set(teacher)
}

最后调用get()方法获取SimpleData< Student>获取它内部封装的Student数据,但是现在SimpleData< Student>中包含的却是一个Teacher实例,那么此时必然会产生类型转换异常
为了杜绝这种安全隐患,Java是不允许使用这种方式传递参数的。即虽然Student是Person的子类,但是SimpleData< Student>并不是SimpleData< Person>的子类。
如果SimpleData在泛型T上是只读的,就不能设置Teacher实例,就不会存在类型转换的安全隐患。
协变的定义
假如定义了一个MyClass< T>的泛型类,其中A是B的子类,同时MyClass< A>又是MyClass< B>的子类,那么我们就可以称MyClass在T这个泛型上是协变的。
实现MyClass< A>又是MyClass< B>的子类,则需要让MyClass< T>类中的所有方法都不能接收T类型的参数,也就是让T只能出现在out位置上,而不能出现在in位置上。
下面改造一下SimpleData类

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

这里在泛型T的声明前面加上out关键字,这就意味着T只能出现在out位置上,同时也意味着SimpleData在泛型T上是协变的。
由于这里我们使用的val关键字,所以构造函数中的泛型T仍然是只读的,另外,即使使用var,只要给它加上private修饰符,保证这个T对于外部是不可修改的,也是合法的写法。
修改测试代码

fun main() {
    val student = Student("tom", 22)
    val data = SimpleData<Student>(student)
    handleSimpleData(data)//这里可以安全传递
    val studentData = data.get()
}

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

开头指出如果某个方法接收一个List< Person>类型的参数,而我们传人一个 List< Student>的实例,在Java中是不允许这么做的,但是在Kotlin中这么做是合法的,因为Kotlin已经默认给许多内置的API加上了协变声明,其中就包括各种集合的类和接口。
Kotlin中的List本身就是只读的,意味着它是可以协变的。如果想要给List添加数据,需要使用MutableList,下面看下List简化版源码

public interface List<out E> : Collection<E> {
    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>
    public operator fun get(index: Int): E
}

List在泛型E的前面加上了out关键字,说明List在泛型E上是协变的。原则上声明了协变后,泛型E就只能出现在out位置上,可是在contains()方法中,泛型E仍然出现在了in位置上。
contains()方法目的非常明确,它只是为了判断当前集合中是否包含参数中传入的这个元素,并不会修改当前集合中的内容,因此这种操作实质上又是安全的。为了让编译器能够理解这种操作是安全的,这里在泛型E的前面又加上了一个@UnsafeVariance注解,这样编译器就会允许泛型E出现在in位置。

4.4 泛型的逆变

逆变的定义
假如定义了一个MyClass< T>的泛型类,其中A是B的子类型,同时MyClass< B>又是MyClass< A>的子类型,那么我们就可以称MyClass在T泛型上是逆变的。
举个例子,先定义一个Transformer接口,用于执行一些转换操作:

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

参数T在经过transform()方法的转换后将会变成一个字符串。至于具体的转换逻辑,则由子类去实现,Transformer接口对此不关心。
对这个接口进行实现:

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", 22)
    val result = trans.transform(student)
}

这段代码从安全的角度来分析是没有问题的,因为Student是Person的子类,使用Transformer< Person>的匿名类实现将Student对象转换成一个字符串是安全的,但是在调用handleTransformer()方法的时候会提示语法错误,因为Transformer< Person>并不是Transformer< Student>的子类。
这时候就要用到逆变,修改Transformer接口中的代码:

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

这里在泛型T的声明前面加上了一个in关键字。意味着现在T只能出现再in位置上,同时也意味着Transformer再泛型T上是逆变的。
修改这个地方后,上面的代码就可以正常编译通过了,因为此时Transformer< Person>成为了Transformer< Student>的子类。
假设,逆变是允许泛型T出现在out位置上的,修改Transformer中的代码:

interface Transformer<in T> {
    fun transform(name: String, age: Int) : @UnsafeVariance T
}

此时会产生什么样的安全隐患呢,如下代码:

fun main() {
    val trans = object : Transformer<Person> {
        override fun transform(name: String, age: Int): Person {
            return Teacher(name, age)
        }
    }
    handleTransformer(trans)
}

fun handleTransformer(trans: Transformer<Student>) {
    val result = trans.transform("Tom", 22)
}

由于transform()方法的返回值要求的是一个Person对象,而Teacher是Person的子类,因此这种写法是合法的。但在handleTransformer()方法中,我们调用了Transformer< Student>的transform()方法,并传入name和age,期望得到一个Student对象的返回,然而实际上transform()方法返回的是一个Teacher对象,因此这里会造成类型转换异常。
逆变功能在Kotlin内置API中的应用
Comparable,是一个用于比较两个对象大小的接口,源码如下:

public interface Comparable<in T> {
    public operator fun compareTo(other: T): Int
}

Comparab在T这个泛型上是逆变的。

七、infix函数

使用infix函数构建更可读的语法
mapOf()函数,使用A to B的语法结构,to并不是Kotlin的关键字,之所以可以使用这种语法结构,是因为Kotlin提供了一种高级语法糖特性:infix函数,其实就是把编程语言函数调用的语法规则调整了一下,比如A to B这样的写法,实际上等价于A.to(B)的写法。
String类中有一个StartsWith()函数,它可以用于判断一个字符串是否是以某个指定的参数开头的。

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

借助infix函数,我们可以使用一种更具可读性的语法来表达这段代码。新建一个infix.kt文件,编写一下代码

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

加上infix关键字后,beginsWith()函数就变成了一个infix函数,这样除了传统的函数调用方式之外,还可以用一种特殊的语法糖格式调用beginsWith()函数

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

infix函数允许我们将函数的小数点、括号等计算机相关的语法去掉,使用一种更接近英语的语法来编写程序,让代码更具可读性。
infix函数有两个比较严格的限制:infix函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;infix函数必须接收且只能接收一个参数。
一个集合,想要判断集合中是否包括某个指定元素,一般这样写

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

在infix.kt文件中添加如下代码

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

我们给Collection接口添加了一个扩展函数,因为Collection是Java和Kotlin所有集合的总接口,因此给Collection添加一个has()函数,那么所有集合的子类就都可以使用这个函数了。
这里使用了泛型函数的定义方法,这样has()函数可以接收任意具体类型的参数。
于是判断集合中是否包括某个指定的元素可以改成

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

mapOf()函数中允许我们使用A to B这样的语法来构建键值对,to()函数源码

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

这里使用泛型函数的定义方式将to()函数定义到A类型下,并且接收一个B类型的参数。因此A和B可以是两种不同类型的泛型,也就可以构建出字符串to整型这样的键值对。
to()函数的实现,创建并返回了一个Pair对象,而mapOf()函数实际上接收的正是一个Pair类型的可变参数列表。

八、协程

简单的将它理解成一种轻量级的线程。协程允许我们在单线程模式下模拟多线程编程效果,代码执行时的挂起与恢复完全由编程语言来控制,和操作系统无关。

1. 协程的基本用法

Kotlin并没有将协程纳入标准库的API中,而是以依赖库的形式提供的。如果需要使用协程功能,需要现在app/build.gradle文件中添加如下依赖:

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
}
1.1 如何开启一个协程

最简单的方式就是使用Global.launch函数:

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
    }
}

GlobalScope.launch函数可以创建一个协程的作用域。以上代码不会有日志打印,因为Global.launch函数每次创建的都是一个顶层协程,这种协程当应用程序运行结束时也会跟着一起结束。
我们让程序延迟一段时间在结束。

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
    }
    Thread.sleep(1000) 
}

Thread.sleep(1000) 让主线程阻塞1秒,日志内容就可以正常打印出来了。
但是如果代码块中的代码在1秒内不能运行结束,那么就会被强制中断。

fun main() {
    GlobalScope.launch {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }
    Thread.sleep(1000)
}

delay()函数可以让当前协程延迟指定时间后再运行,但它和Thread.sleep()方法不同。delay()是一个非阻塞式的挂起函数,它只挂起当前协程,并不会影响其他协程的运行。而Thread.sleep()方法会阻塞当前线程,这样运行在当前线程下的所有协程都会被阻塞。注意,delay()函数只能在协程的作用域或其他挂起函数中调用。
所以上述代码新增的打印不会被打印出来,因为协程挂起1.5秒,而主线程却只阻塞了1秒。
借助runBlocking函数就可以让应用程序在协程中所有代码都运行完之后再结束。

fun main() {
    runBlocking {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }
}

runBlocking函数同样会创建一个协程的作用域,但是它可以保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。注意,runBlocking函数通常只应该在测试环境使用,在正式环境中使用容易产生一些性能上的问题。

1.2 如何开启多个协程

使用launch函数,如下:

fun main() {
    runBlocking {
        launch {
            println("launch1")
            delay(1000)
            println("launch1 finished")
        }
        launch {
            println("launch2")
            delay(1000)
            println("launch2 finished")
        }
    }
}

这里的launch函数和之前的GlobalScope.launch函数不同。首先它必须在协程的作用域中才能调用,其次它会在当前协程的作用域下创建子协程。子协程的特点是如果外层作用域的协程结束了,该作用域下的所有子协程也会一同结束。相比而言,GlobalScope.launch函数创建的永远是顶层协程。
两个子协程是交替打印的,说明它们像多线程那样并发运行的。

fun main() {
    val start = System.currentTimeMillis()
    runBlocking {
        repeat(100000) {
            launch {
                println(".")
            }
        }
    }
    val end = System.currentTimeMillis()
    println("----${end -start}----")
}

这里使用repeat函数循环创建了10万个协程,并记录整个操作的运行耗时,结果发现只需804毫秒,足以证明协程的高效开发。

1.3 挂起函数

一般launch函数中的逻辑复杂后,需要将部分代码提取到一个单独的函数中。这个时候就会有个问题:在launch函数中编写的代码是拥有协程作用域的,但是提取到一个单独的函数中就没有协程作用域了,那么该如何调用像delay()这样的挂起函数呢?
为此,Kotlin提供了一个suspend关键字,使用它可以将任意函数声明成挂起函数,而挂起函数之间都是可以相互调用的:

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

suspend关键字只能将一个函数声明成挂起函数,是无法给它提供协程作用域的。
这个问题可以借助coroutineScope函数来解决。coroutineScope函数也是一个挂起函数,因此可以在任何其他挂起函数中调用。它的特点是会继承外部的协程作用域并创建一个子作用域,这样我们可以给任意的挂起函数提供协程作用域了。

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

coroutineScope函数和runBlocking函数有点类似,它可以保证其作用域内的所有代码和子协程在全部执行完之前,会一直阻塞当前协程:

fun main() {
    runBlocking {
        coroutineScope {
            launch {
                for(i in 1..10) {
                    println(i)
                    delay(1000)
                }
            }
        }
        println("coroutineScope finished")
    }
    println("runBlocking finished")
}

控制台每隔1秒依次输出数字1到10,然后打印coroutineScope函数的结尾日志,最后打印runBlocking函数的结尾日志。
虽然两函数作用非常类似,但是coroutineScope函数只会阻塞当前协程,既不影响其他协程,也不影响任何线程,因此不会造成任何性能上的问题。而runBlocking函数由于会阻塞当前线程,如果在主线程中调用,有可能会导致界面卡死。

2. 更多的作用域构建器

作用域构建器 任意地方调用 协程作用域 挂起函数中
GlobalScope.launch    
runBlocking    
launch    
coroutineScope  

其中runBlocking由于会阻塞线程,只建议在测试环境中使用,GlobalScope.launch每次创建的都是顶层协程,也不建议使用,因为管理起来成本太高。比如我们在某个Activity中使用协程发起了一条网络请求,由于网络请求是耗时的,用户在服务器还没来得及响应的情况下就关闭了当前Activity,此时按理说应该取消这条网络请求,或者不应该进行回调。
协程怎么取消呢?不管是GlobalScope.launch函数还是launch函数,它们都会返回一个Job对象,只需调用Job对象的cancel()方法就可以取消协程了,如下:

val job = GlobalScope.launch { 
        //处理具体逻辑
    }
    job.cancel()

但是如果我们每次创建的都是顶层协程,那么当Activity关闭时,就需要逐个调用所有已创建协程的cancel()方法,无法维护。
实际项目中比较常用的协程作用域构建器的方法:

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

将创建的Job对象传入CoroutineScope()函数中,返回一个CoroutineScope对象,调用它的launch函数创建一个协程。这样所创建的协程都会被关联在Job对象的作用域下,所以只需调用一次cancel()方法,就可以将同一作用域内的所有协程全部取消。
调用launch函数可以创建一个新协程,但是launch函数只是执行一段逻辑,却不能获取执行结果,因为它的返回值永远是一个Job对象。
async函数可以获取执行结果,它必须在协程作用域当中才能调用,它会创建一个新协程并返回一个Deferred对象,如果想要获取async函数代码块的执行结果,只需调用Deferred对象的await()方法即可,,代码如下:

fun main() {
    runBlocking {
        val result = async {
            5   5
        }.await()
        println(result)
    }
}

在调用了async函数之后,代码块中的代码就会立刻执行。当调用await()方法时,如果代码块中的代码还没执行完,那么await()方法会将当前协程阻塞住,直到可以获得async函数的执行结果。

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val result = async {
            delay(1000)
            5   5
        }.await()
        val result2 = async {
            delay(1000)
            3   6
        }.await()
        println("result is ${result   result2}.")
        val end = System.currentTimeMillis()
        println("cost ${end - start}ms.")
    }
}
学新通

整段代码运行耗时2027毫秒,说明这里两个async函数确实是一种串行的关系,前一个执行完后一个才能执行。
这种写法非常低效,两个async函数完全可以同时执行,修改以上代码:

fun main() {
    runBlocking {
        val start = System.currentTimeMillis()
        val deferred1 = async {
            delay(1000)
            5   5
        }
        val deferred2 = async {
            delay(1000)
            3   6
        }
        println("result is ${deferred1.await()   deferred2.await()}.")
        val end = System.currentTimeMillis()
        println("cost ${end - start}ms.")
    }
}
学新通

整段代码运行耗时1010毫秒,运行效率明显提升。
withContext()函数也是一个挂起函数,大体可以理解成async函数的一种简化版:

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

调用withContext函数之后,会立即执行代码块中的代码,同时将当前协程阻塞住。当代码块中的代码全部执行完,会将最后一行的执行结果作为withContext()函数的返回值返回,相当于val result = async { 5 5}.await()的写法。唯一的不同是,withContext()函数强制要求我们指定一个线程参数。
传统编程情况下需要开启多线程执行并发任务,现在只需要在一个线程下开启多个协程来执行就可以了。但是并不意味着我们就永远不需要开启线程了,比如说Android中要求网络请求必须在子线程中进行,即使你开启了协程求执行网络请求,假如它是在主线程当中的协程,那么程序仍然会出错。这个时候我们就应该通过线程参数给协程指定一个具体的运行线程。
线程参数主要有以下3种值可选:Dispatchers.Default、Dispatchers.IO和Dispatchers.Main。Dispatchers.Default表示会使用一种默认低并发的线程策略,当你要执行的代码属于计算密集型任务时,开启过高的并发反而可能会影响任务的运行效率,此时就可以使用Dispatchers.Default。Dispatchers.IO表示会使用一种较高的线程策略,当你要执行代码大多数时间是在阻塞和等待中,比如说执行网络请求时,为了能够支持更高的并发数量,此时就可以使用Dispatchers.IO。Dispatchers.Main则表示不会开启子线程,而是在Android主线程中执行代码,但是这个值只能在Android项目中使用,纯Kotlin程序使用这种类型的线程参数会出现错误。
以上协程作用域构建器中,除了coroutineScope函数之外,其他所有的函数都是可以指定这样一个线程参数的,只是withContext()函数是强制要求指定的,其他函数是可选的。

3. 使用协程简化回调的写法

编程语言的回调机制基本是依靠匿名类来实现的

HttpUtil.sendHttpRequest(address, object : HttpCallbackListener {
    override fun onFinish(response: String) {
        //得到服务器返回的具体内容
    }
    override fun onError(e: Exception) {
        //在这里对异常情况进行处理
    }
})

现在借助suspendCoroutine函数就能将传统回调机制优化。
suspendCoroutine函数必须在协程作用域或挂起函数中才能调用,它接收一个Lambda表达式参数,主要作用是将当前协程立即挂起,然后在一个普通的线程中执行Lambda表达式中的代码。Lambda表达式的参数列表上会传入一个Continuation参数,调用它的resume()方法或者resumeWithException()方法可以让协程恢复执行。

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

//调用,不需要重复进行回调实现
suspend fun getBaiduResponse() {
    try {
        val response = request("https://www.百度.com/)
        //对服务器响应的数据进行处理
    } catch(e: Exception) {
        //对异常情况进行处理
    }
}
学新通

由于getBaiduResponse()是一个挂起函数,因此当它调用了request()函数时,当前协程就会被立刻挂起,然后一直等待网络请求成功或失败后,当前协程才能恢复运行。
不过getBaiduResponse()是一个挂起函数,这样它只能在协程作用域或者其他挂起函数中调用。不过通过合理的项目架构设计,可以轻松的将各种协程的代码应用到一个普通的项目中。
suspendCoroutine函数几乎可以用于简化任何回调写法,比如以前Retrofit来发起网络请求

val appService = ServiceCreator.create<AppService>()
appService.getAppData().enqueue(object: Callback<List<App>> {
    override fun onResponse(call: Call<List<App>>, response: Response<List<App>>) {
        //得到服务器返回的数据
    }
    override fun onFailure(call: Call<List<App>>, t: Throwable) {
        //在这里对异常情况进行处理
    }
})

由于不同的Service接口返回的数据类型也不同,所以需要使用泛型。

suspend fun <T> Call<T>.await() : T {
    return suspendCoroutine { continuation ->
        enqueue(object: Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                val body = response.body()
                if(body != null) {
                    continuation.resum(body)
                }else {
                    continuation.resumWithException(RuntimeException("response body is null"))
                }
            }
            override fun onFailure(call: Call<List<App>>, t: Throwable) {
                continuation.resumeWithException(t)
            }
        })
}

//调用
suspend fun getAppData() {
    try {
        val appList = ServiceCreator.create<AppService>().getAppData().await()
        //对服务器响应的数据进行处理
    } catch (e: Exception) {
        //对异常情况进行处理
    }
}
学新通

九、DSL

领域特定语言(Domain Specific Language),t它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。

十、Java与Kotlin之间的转换

1.Java转化成Kotlin

  • . 复制java的一段代码,然后在Android Studio中打开任意一个Kotlin文件,在这里进行粘贴。(这里是按照固定的语法变化规律来执行转换工作,不会自动应用Kotlin的各种优秀特性)
  • Java文件以及其中的所有代码一次性转换成Kotlin版本,在Android Studio中打开该Java文件,然后点击导航栏的Code->Convert Java File to Kotlin File

2.Kotlin转化成Java

Android Studio没有提供类似的转换功能,因为Kotlin拥有许多Java中并不存在的特性。
不过,我们可以先将Kotlin代码转换成Kotlin字节码,然后通过反编译的方式将它还原成Java代码,这个方法可以帮助我们理解Kotlin特性背后的原理。
由于kotlin-android-extensions插件,我们不需要通过findViewById()方法去获取控件的实例。
打开Kotlin文件,在Android Studio导航栏中Tools->Kotlin->Show Kotlin Bytecode。会显示Kotlin文件的字节码内容,然后点击左上角的Decompile按钮,就可以将这些Kotlin字节码反编译成Java代码。

使用Kotlin编写的Android项目在app/build.gradle文件的头部默认引入了一个kotlin-android-extensions插件,这个插件会根据布局文件中定义的控件id自动生成一个具有相同名称的变量。
Kotlin中SecondActivity::class.java的写法相当于Java中SecondActivity.class的写法。
Kotlin中使用inner class关键字来定义内部类。
Kotlin允许我们将没有用到的参数使用下划线来替代。
定义常量的关键字是const,注意只有在单例类、companion object或顶层方法中才可以使用const。
Kotlin中的类型强制转换使用的关键字是as
use函数,Kotlin内置扩展函数。它会保证在Lambda表达式中的代码全部执行完之后自动将外层的流关闭。
vararg对应的是Java中的可变参数列表。
Kotlin取消了按位运算符的写法,改成了使用英文关键字,and相当于Java中的&运算符,or相当于Java中的|运算符,xor相当于Java中的^运算符。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhhaaejj
系列文章
更多 icon
同类精品
更多 icon
继续加载