Kotlin基础 -- 3

2020-08-29  本文已影响0人  TomyZhang

九、泛型

1.泛型类型参数

泛型有以下几点优势:

泛型函数和属性

调用泛型函数:

fun <T> List<T>.slice(indices: IntRange): List<T>

fun method() {
    val letters = ('a'..'z').toList()
    val result = letters.slice<Char>(0..2)
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-13 09:13:31.015 21950-21950/com.tomorrow.kotlindemo D/TAG: zwm, result: [a, b, c]

调用泛化的高阶函数:

fun <T> List<T>.filter(predicate: (T) -> Boolean): List<T>

fun method() {
    val authors = listOf("Kotlin", "Java", "Shell")
    val readers = listOf("Kotlin", "Java")
    val result = readers.filter { it in authors }
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-13 09:17:46.413 22437-22437/com.tomorrow.kotlindemo D/TAG: zwm, result: [Kotlin, Java]

可以给类或接口的方法、顶层函数,以及扩展函数声明类型参数。

还可以用同样的语法声明泛型的扩展属性:

val <T> List<T>.penultimate: T //这个泛型扩展属性能在任何种类元素的列表上调用
    get() = this[size - 2]

fun method() {
    val list = listOf(1, 2, 3, 4)
    Log.d("TAG", "zwm, result: ${list.penultimate}") //在这次调用中,类型参数T被推导成Int
}

日志打印:
2020-08-13 10:12:39.700 30015-30015/com.tomorrow.kotlindemo D/TAG: zwm, result: 3

普通(即非扩展)属性不能拥有类型参数,不能在一个类的属性中存储多个不同类型的值,因此声明泛型非扩展属性没有任何意义。

声明泛型类

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

class Person : Comparable<Person> {
    override fun compareTo(other: Person): Int {
        return 0
    }
}

类型参数约束

如果你把一个类型指定为泛型类型形参的上界约束,在泛型类型具体的初始化中,其对应的类型实参就必须是这个具体类型或者它们的子类型。

fun <T : Number> List<T>.sum(): T

声明带类型参数约束的函数:

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

fun <T: Comparable<T>> max(first: T, second: T): T {
    return if (first > second) first else second
}

为一个类型参数指定多个约束:

fun <T> ensureTrailingPeriod(seq: T)
    where T: CharSequence, T: Appendable {
    if(!seq.endsWith('.')) { //调用为CharSequence接口定义的扩展函数
        seq.append('.') //调用Appendable接口的方法
    }
}

fun method() {
    val hello = StringBuilder("Hello World")
    ensureTrailingPeriod(hello)
    Log.d("TAG", "zwm, hello: $hello")
}

日志打印:
2020-08-13 11:20:41.561 2827-2827/com.tomorrow.kotlindemo D/TAG: zwm, hello: Hello World.

让类型形参非空

如果你声明的是泛型类或者泛型函数,任何类型实参,包括那些可空的类型实参,都可以替换它的类型形参。事实上,没有指定上界的类型形参都会使用Any?这个默认的上界:

class Procesor<T> {
    fun process(value: T) {
        value?.equals(value) //value是可空的,需要使用安全调用
    }
}

如果你想保证替换类型形参的始终是非空类型,可以通过指定一个约束来实现。如果你除了可空性之外没有任何限制,可以使用Any代替默认的Any?作为上界:

class Procesor<T : Any> {
    fun process(value: T) {
        value.equals(value)
    }
}

注意,可以通过指定任意非空类型作为上界,来让类型参数非空,不光是类型Any。

2.运行时的泛型:擦除和实化类型参数

运行时的泛型:类型检查和转换

不能判断一个列表是一个包含字符串的列表还是包含其他对象的列表:

fun method() {
    val value = listOf(1)
    if(value is List<String>) { //编译错误
        //
    }
}

可以使用特殊的星号投影语法来判断一个值是否是列表,而不是set或者其他对象:

fun method() {
    val value = setOf(1)
    if(value is List<*>) { //编译成功
        //
    }
}

对泛型类型做类型转换:

fun printSum(c: Collection<*>) {
    val intList = c as? List<Int> //这里会有警告:Unchecked cast: Collection<*> to List<Int>
            ?: throw IllegalArgumentException("List is expected")
    Log.d("TAG", "zwm, result: ${intList.sum()}")
}

fun method() {
    printSum(listOf(1, 2 ,3 )) //一切都符合预期
    printSum(setOf(1, 2 ,3 )) //Set不是List,抛出了异常:IllegalArgumentException
    printSum(listOf("a", "b", "c")) //类型转换成功,但后面抛出了另外的异常:ClassCastException
}

对已知类型实参做类型转换:

fun printSum(c: Collection<Int>) {
    val intList = c as? List<Int> //这次检查是合法的
            ?: throw IllegalArgumentException("List is expected")
    Log.d("TAG", "zwm, result: ${intList.sum()}")
}

fun method() {
    printSum(listOf(1, 2 ,3 )) //一切都符合预期
}

声明带实化类型参数的函数:内联函数的类型参数可以被实化

inline fun <reified T> isA(value: Any) = value is T //现在代码可以编译了

fun method() {
    val result = isA<String>("abc")
    Log.d("TAG", "zwm, result: $result")
    val result2 = isA<String>(123)
    Log.d("TAG", "zwm, result2: $result2")
}

日志打印:
2020-08-13 14:32:55.903 22975-22975/com.tomorrow.kotlindemo D/TAG: zwm, result: true
2020-08-13 14:32:55.903 22975-22975/com.tomorrow.kotlindemo D/TAG: zwm, result2: false

使用标准库函数filterIsInstance:

fun method() {
    val items = listOf("one", 2, "three")
    val result = items.filterIsInstance<String>()
    Log.d("TAG", "zwm, result: $result")
}

日志打印:
2020-08-13 15:20:26.819 27011-27011/com.tomorrow.kotlindemo D/TAG: zwm, result: [one, three]

使用实化类型参数代替类引用:

inline fun <reified T> loadService(): ServiceLoader<T> { //类型参数标记成了reified
    return ServiceLoader.load(T::class.java) //把T::class当成类型形参的类访问
}

简化Android上的startActivity函数:

inline fun <reified T: Activity> Context.startActivity() { //类型参数标记成了reified
    val intent = Intent(this, T::class.java) //把T::class当成类型形参的类访问
    startActivity(intent)
}

startActivity<MainActivity>() //调用方法显示Activity

实化类型参数的限制

可以按下面的方式使用实化类型参数:

不能做下面这些事情:

类型擦除的矛盾

fun method() {
    //val listType = ArrayList<String>::class.java //编译错误
    //val mapType = Map<String, String>::class.java //编译错误
    
    val list1 = ArrayList<String>()
    val list2 = object : ArrayList<String>() { } //匿名内部类
    Log.d("TAG", "zwm, list1 type: ${list1.javaClass.genericSuperclass}")
    Log.d("TAG", "zwm, list2 type: ${list2.javaClass.genericSuperclass}")
}

日志打印:
2020-08-21 08:27:21.426 14785-14785/com.tomorrow.kotlindemo D/TAG: zwm, list1 type: java.util.AbstractList<E> //不能获取泛型参数的类型
2020-08-21 08:27:21.426 14785-14785/com.tomorrow.kotlindemo D/TAG: zwm, list2 type: java.util.ArrayList<java.lang.String> //能够获取泛型参数的类型

其实泛型类型擦除并不是真的将全部的类型信息都擦除,还是会将类型信息放在对应class的常量池中的。匿名内部类在初始化的时候就会绑定父类或父接口的相应信息,这样就能通过获取父类或父接口的泛型类型信息来实现我们的需求。

使用内联函数获取泛型

Kotlin中的内联函数在编译的时候编译器便会将相应函数的字节码插入调用的地方,也就是说,参数类型也会被插入字节码中,我们就可以获取参数的类型了。

inline fun <reified T> getType(): Class<T> {
    return T::class.java
}

fun method() {
    Log.d("TAG", "zwm, type: ${getType<String>()}")
}

日志打印:
2020-08-21 08:41:15.650 15197-15197/com.tomorrow.kotlindemo D/TAG: zwm, type: class java.lang.String

Kotlin中声明的普通内联函数可以在Java中调用,因为它会被当作一个常规函数。而用reified来实例化的参数类型的内联函数则不能在Java中调用,因为它永远是需要内联的。

3.变型:泛型和子类型化

协变

如果A是B的子类型,并且Generic<A>也是Generic<B>的子类型,那么Generic<T>可以称之为一个协变类。

Java的协变通过上界通配符(<? extends T>)实现:

public class JavaDemo {

    class Animal {

    }

    class Dog extends Animal {

    }

    public void testDemo() {
        List<Animal> animals = new ArrayList<>();
        List<Dog> dogs = new ArrayList<>();
        animals = dogs; //编译错误:incompatible types
    }
}
public class JavaDemo {

    class Animal {

    }

    class Dog extends Animal {

    }

    public void testDemo() {
        List<? extends Animal> animals = new ArrayList<>(); //上界通配符
        List<Dog> dogs = new ArrayList<>();
        animals = dogs; //编译成功
    }
}

Kotlin的协变通过out关键字实现:

interface Producer<out T> {
    fun produce(): T
}
fun method() {
    var animals: List<Animal> = ArrayList()
    val dogs = ArrayList<Dog>()
    animals = dogs //编译成功:Kotlin的List源码中使用了out关键字,相当于Java的上界通配符
}

in位置与out位置:

interface Transform<T> {
    fun transform(t: T): T //函数参数的类型叫作in位置,而函数返回类型叫作out位置
}

类型参数上的关键字out有两层含义:

逆变

如果A是B的子类型,并且Generic<B>是Generic<A>的子类型,那么 Generic<T>可以称之为一个逆变类。

Java的逆变通过下界通配符(<? super T>)实现:

public class JavaDemo {

    class Animal {

    }

    class Dog extends Animal {

    }

    public void testDemo() {
        List<? extends Animal> animals = new ArrayList<>();
        animals.add(new Dog()); //编译错误
    }
}
public class JavaDemo {

    class Animal {

    }

    class Dog extends Animal {

    }

    public void testDemo() {
        List<? super Animal> animals = new ArrayList<>(); //下界通配符
        animals.add(new Dog()); //编译成功
    }
}

Kotlin的逆变通过in关键字实现:

interface Comparator<in T> {
    fun compare(e1: T, e2: T): Int { ... }
}

协变的、逆变的和不变型的类:

协变 逆变 不变型
Producer<out T> Consumer<int T> MutableList<T>
类的子类型化保留了:Producer<Cat>是Producer<Animal>的子类型 子类型化反转了:Consumer<Animal>是Consumer<Cat>的子类型 没有子类型化
T只能在out位置 T只能在in位置 T可以在任何位置

一个类可以在一个类型参数上协变,同时在另外一个类型参数上逆变:

interface Function1<in P, out R> {
    operator fun invoke(p: P): R
}

总结:

使用点变型:在类型出现的地方指定变型

带不变形类型参数的数据拷贝函数:

fun <T> copyData(source: MutableList<T>, destination: MutableList<T>) {
    for(item in source) {
        destination.add(item)
    }
}

带不变型类型参数的数据拷贝函数:

fun <T: R, R> copyData(source: MutableList<T>, destination: MutableList<R>) {
    for(item in source) {
        destination.add(item)
    }
}

fun method() {
    val source = mutableListOf(1, 2, 3)
    val destination = mutableListOf<Any>()
    copyData(source, destination)
    Log.d("TAG", "zwm, destination: $destination")
}

日志打印:
2020-08-13 20:13:14.130 7675-7675/com.tomorrow.kotlindemo D/TAG: zwm, destination: [1, 2, 3]

带out投影类型参数的数据拷贝函数:

fun <T> copyData(source: MutableList<out T>, destination: MutableList<T>) {
    for(item in source) {
        destination.add(item)
    }
}

fun method() {
    val source = mutableListOf(1, 2, 3)
    val destination = mutableListOf<Any>()
    copyData(source, destination)
    Log.d("TAG", "zwm, destination: $destination")
}

日志打印:
2020-08-13 20:15:46.047 7868-7868/? D/TAG: zwm, destination: [1, 2, 3]

带in投影类型参数的数据拷贝函数:

fun <T> copyData(source: MutableList<T>, destination: MutableList<in T>) {
    for(item in source) {
        destination.add(item)
    }
}

fun method() {
    val source = mutableListOf(1, 2, 3)
    val destination = mutableListOf<Any>()
    copyData(source, destination)
    Log.d("TAG", "zwm, destination: $destination")
}

日志打印:
2020-08-13 20:21:12.138 8405-8405/com.tomorrow.kotlindemo D/TAG: zwm, destination: [1, 2, 3]

星号投影:使用*代替类型参数

Kotlin的MyType<*>对应于Java的MyType<?>:

fun method() {
    val list: MutableList<Any?> = mutableListOf('a', 1, "abc")
    val chars = mutableListOf('a', 'b', 'c')
    val unknownElements: MutableList<*> = if(Random.nextBoolean()) list else chars
    //unknownElements.add("test") //编译错误
    Log.d("TAG", "zwm, result: ${unknownElements.first()}")
}

日志打印:
2020-08-13 20:36:16.823 9117-9117/com.tomorrow.kotlindemo D/TAG: zwm, result: a

MutableList<*>投影成了MutableList<out Any?>:当你没有任何元素类型信息的时候,读取Any?类型的元素仍然是安全的,但是向列表中写入元素是不安全的。

当类型实参的信息并不重要的时候,可以使用星号投影的语法:不需要使用任何在签名中引用类型参数的方法,或者只是读取数据而不关心它的具体类型:

fun printFirst(list: List<*>) { //每一种列表都是可能的实参
    if(list.isNotEmpty()) { //isNotEmpty()没有使用泛型类型参数
        println(list.first()) //first()现在返回的是Any?,但是这里足够了
    }
}

fun method() {
    printFirst(listOf("zwm", 999))
}

日志打印:
2020-08-13 20:52:27.593 10213-10213/com.tomorrow.kotlindemo I/System.out: zwm

在使用点变型的情况下,你有一个替代方案:引入一个泛型类型参数:

fun <T> printFirst(list: List<T>) { //再一次,每一种列表都是可能的实参
    if(list.isNotEmpty()) {
        println(list.first()) //first()现在返回的是T的值
    }
}

fun method() {
    printFirst(listOf("zwm", 999))
}

日志打印:
2020-08-13 20:57:11.746 10583-10583/com.tomorrow.kotlindemo I/System.out: zwm

十、注解与反射

1.声明并应用注解

应用注解

@Deprecated("Use newMethod(index) instead.", ReplaceWith("newMethod(index)"))
fun oldMethod(index: Int) {
    Log.d("TAG", "zwm, this is old method: $index")
}

fun newMethod(index: Int) {
    Log.d("TAG", "zwm, this is old method: $index")
}

fun method() {
    oldMethod(968)
}

日志打印:
2020-08-14 20:25:43.242 10366-10366/com.tomorrow.kotlindemo D/TAG: zwm, this is old method: 968

实参在括号中传递,就和常规函数的调用一样。注解只能拥有如下类型的参数:基本数据类型、字符串、枚举、类引用、其他的注解类,以及前面这些类型的数组。指定注解实参的语法与Java有些微小的差别:

注解实参需要在编译期就是已知的,所以你不能引用任意的属性作为实参。要把属性当作注解实参使用,你需要用const修饰符标记它,来告知编译器这个属性是编译期常量。

注解目标

使用点目标声明被用来说明要注解的元素,使用点目标被放在@符号和注解名称之间,并用冒号和注解名称隔开。例如,@get:Rule,注解@Rule被应用到了属性的getter上。

Kotlin支持的使用点目标的完整列表如下:

任何应用到file目标的注解都必须放在文件的顶层,放在package指令之前。@JvmName是常见的应用到文件的注解之一,它改变了对应类的名称。例如,@file:JvmName("StringFunctions")。

注意,和Java不一样的是,Kotlin允许你对任意的表达式应用注解,而不仅仅是类和函数的声明及类型。最常见的例子就是@Suppress注解,可以用它抑制被注解的表达式的上下文中的特定的编译器警告。

fun test(list: List<*>) {
    @Suppress("UNCHECKED CAST")
    val strings = list as List<String>
    //...
}

用注解控制Java API:

Kotlin提供了各种注解来控制Kotlin编写的声明如何编译成字节码并暴露给Java调用者。其中一些注解代替了Java语言中对应的关键字:比如,注解@Volatile和@Strictfp直接充当了Java的关键字volatile和strictfp的替身。其他的注解则是被用来改变Kotlin声明对Java调用者的可见性:

声明注解

注解类的声明是这样的,它是一个 拥有主构造方法且没有类主体的类,其构造方法中所有参数都被标记成val属性。

annotation class JsonExclude
annotation class JsonName(val name: String)

元注解:控制如何处理一个注解

元注解可以用来指定(使用点)目标、保留期模式和其他注解的特性。

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

AnnotationTarget枚举的值列出了可以应用注解的全部可能的目标。包括:类、文件、函数、属性、属性访问器、所有表达式等等。如何需要,你还可以声明多个目标:@Target(AnnotationTarget.CLASS, AnnotationTarget.METHOD)。

要声明自己的元注解,使用ANNOTATION_CLASS作为目标就好了:

@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation

@BindingAnnotation
annotation class MyBinding

注意,在Java代码中无法使用目标为PROPERTY的注解,要让这样的注解可以在Java中使用,可以给它添加第二个目标AnnotationTarget.FIELD。这样注解既可以应用到Kotlin中的属性上,也可以应用到Java中的字段上。

@Retention注解:

它被用来说明你声明的注解是否会存储到.class文件,以及在运行时是否可以通过反射来访问它。Java默认会在.class文件中保留注解但不会让它们在运行时被访问到。大多数注解确实需要在运行时存在,所以Kotlin的默认行为不同,注解拥有RUNTIME保留期。

使用类做注解参数

annotation class MyAnnotation(val targetClass: KClass<out Any>)

data class Person(val name: String, val age: Int)
data class Company(val name: String, @MyAnnotation(Person::class) val person: Person)

KClass是Java的java.lang.Class类型在Kotlin中的对应类型,它用来保存Kotlin类的引用。

out关键字说明允许引用那些继承Any的类,而不仅仅是引用Any自己。

使用泛型类做注解参数

interface Value<T> {
    fun test(value: Any?): T
}

annotation class MyAnnotation(val targetClass: KClass<out Value<*>>)

data class Person(val name: String, val age: Int): Value<String> {
    override fun test(value: Any?): String {
        return "zwm"
    }
}
data class Company(val name: String, @MyAnnotation(Person::class) val person: Person)

2.反射:在运行时对Kotlin对象进行自省

反射,简单来说,一种在运行时动态地访问对象属性和方法的方式,而不需要事先确定这些属性是什么。当在Kotlin中使用反射时,你会和两种不同的反射API打交道。第一种是标准的Java反射,定义在包java.lang.reflect中。因为Kotlin类会编译成普通的Java字节码,Java反射API可以完美地支持它们。实际上,这意味着使用了反射API的Java库完全兼容Kotlin代码。第二种是Kotlin反射API,定义在包kotlin.reflect中。它让你能访问那些在Java世界里不存在的概念,诸如属性和可空类型。

Kotlin反射API:KClass、KCallable、KFunction和KProperty

Kotlin反射API的主要入口就是KClass,它代表了一个类。KClass对应的是java.lang.class,可以用它列举和访问类中包含的所有声明,然后是它的超类中的声明,等等。MyClass::class的写法会带给你一个KClass的实例。要在运行时取得一个对象的类,实现使用javaClass属性获得它的Java类,这直接等价于Java中的java.lang.Object.getClass()。然后访问该类的.kotlin扩展属性,从Java切换到Kotlin的反射API:

class Person(val name: String, val age: Int)

fun method() {
    val person = Person("Kotlin", 10)
    val kClass = person.javaClass.kotlin //返回一个KClass<Person>的实例
    Log.d("TAG", "zwm, simpleName: ${kClass.simpleName}")
    kClass.memberProperties.forEach {     Log.d("TAG", "zwm, property: ${it.name}") }
}

日志打印:
2020-08-15 08:47:54.896 23812-23812/com.tomorrow.kotlindemo D/TAG: zwm, simpleName: Person
2020-08-15 08:47:55.610 23812-23812/com.tomorrow.kotlindemo D/TAG: zwm, property: age
2020-08-15 08:47:55.610 23812-23812/com.tomorrow.kotlindemo D/TAG: zwm, property: name

KClass的声明:

interface KClass<T : Any> : 

   /**
     * The simple name of the class as it was declared in the source code,
     * or `null` if the class has no name (if, for example, it is an anonymous object literal).
     */
    public actual val simpleName: String?

    /**
     * The fully qualified dot-separated name of the class,
     * or `null` if the class is local or it is an anonymous object literal.
     */
    public val qualifiedName: String?

    /**
     * All functions and properties accessible in this class, including those declared in this class
     * and all of its superclasses. Does not include constructors.
     */
    override val members: Collection<KCallable<*>>

    /**
     * All constructors declared in this class.
     */
    public val constructors: Collection<KFunction<T>>

    /**
     * All classes declared inside this class. This includes both inner and static nested classes.
     */
    public val nestedClasses: Collection<KClass<*>>
...

类的所有成员组成的列表是一个KCallable实例的集合。KCallable是函数和属性的超接口,它声明了call方法,允许你调用对应的函数或者对应的属性的getter:

fun foo(x: Int) = Log.d("TAG", "zwm, print: $x")

fun method() {
    val kFunction = ::foo
    kFunction.call(88)
}

日志打印:
2020-08-15 09:01:33.975 24894-24894/com.tomorrow.kotlindemo D/TAG: zwm, print: 88

::foo表达式的类型是KFunction1<Int, Int>,它包含了形参类型和返回类型的信息,1表示这个函数接收一个形参。你使用invoke方法通过这个接口来调用函数。它接收固定数量的实参,而且这些实参的类型对应这KFunction1接口的类型形参,也可以直接调用kFunction:

fun foo(x: Int) = Log.d("TAG", "zwm, print: $x")

fun method() {
    val kFunction: KFunction1<Int, Int> = ::foo
    kFunction.call(66)
    kFunction(77)
    kFunction.invoke(88)
}

日志打印:
2020-08-15 09:14:52.883 25499-25499/com.tomorrow.kotlindemo D/TAG: zwm, print: 66
2020-08-15 09:14:52.883 25499-25499/com.tomorrow.kotlindemo D/TAG: zwm, print: 77
2020-08-15 09:14:52.883 25499-25499/com.tomorrow.kotlindemo D/TAG: zwm, print: 88

也可以在一个KProperty实例上调用call方法,它会调用该属性的getter。但是属性接口为你提供了一个更好的获取属性值的方式:get方法。要访问get方法,需要根据属性声明的方式来使用正确的属性接口。顶层属性表示为KProperty0接口的实例,它有一个无参数的get方法:

var count= 0
fun method() {
    val kProperty = ::count
    kProperty.setter.call(21) //通过反射调用setter
    Log.d("TAG", "zwm, value: ${kProperty.get()}") //通过调用get获取属性的值
}

日志打印:
2020-08-15 09:25:59.394 25926-25926/com.tomorrow.kotlindemo D/TAG: zwm, value: 21

一个成员属性由KProperty1的实例表示,它拥有一个单参数的get方法。要访问该属性的值,必须提供你需要的值所属的那个对象实例:

class Person(val name: String, val age: Int)

var count= 0
fun method() {
    val person = Person("Alice", 3)
    val memberProperty = Person::age
    Log.d("TAG", "zwm, value: ${memberProperty.get(person)}")
}

日志打印:
2020-08-15 09:30:31.781 26437-26437/com.tomorrow.kotlindemo D/TAG: zwm, value: 3

需要注意的是,只能使用反射访问定义在最外层或者类中的属性,而不能访问函数的局部变量。

KCallable.callBy方法能用来调用带默认参数值的方法:

fun foo(x: Int, y: Int = 10) = Log.d("TAG", "zwm, print: $x $y")

fun method() {
    val kFunction  = ::foo
    val params = kFunction.parameters
    val map = mapOf<KParameter, Any?>( params[0] to 999)
    kFunction.callBy(map)
}

日志打印:
2020-08-15 10:40:30.025 27647-27647/com.tomorrow.kotlindemo D/TAG: zwm, print: 999 10

Kotlin反射API中的接口层级结构:

Kotlin反射API中的接口层级结构

十一、异步和并发

1.Kotlin的Coroutine

多线程一定优于单线程吗

多线程在执行的时候,只是看上去是同时执行的,因为线程的执行是通过CPU来进行调度的,CPU通过在每个线程之间快速切换,使得其看上去是同时执行的一样。其实CPU在某一个时间片内只能执行一个线程,当这个线程执行一会儿之后,它就会去执行其他线程。当CPU从一个线程切换到另一个线程的时候会执行许多操作,主要有如下两个操作:

注意,这种切换所产生的开销是不能忽视的,当线程池中的线程有许多被阻塞住了,CPU就会将该线程挂起,去执行别的线程,那么就产生了线程切换。当切换很频繁的时候,就会消耗大量的资源在切换线程操作上,这就会导致一个后果——采用多线程的实现方式并不优于单线程。

协程:一个更轻量级的线程

协程是一个无优先级的子程序调度组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够,一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。协程是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统去进行调度。这样的话就大大降低了开销。

fun method() {
    runBlocking {
        repeat(100_000) { //启动10万个协程执行操作,能正常运行,如果是线程可能会出现“out of memory”的错误了
            launch {
                Log.d("zwm", "World")
            }
        }
    }
    Log.d("zwm", "Hello")
}

合理地使用协程

launch:

fun method() {
    GlobalScope.launch { //在后台启动一个协程
        delay(1000L) //延迟1秒(非阻塞)
        Log.d("zwm", "World: ${Thread.currentThread().name}") //延迟之后输出
    }
    Log.d("zwm", "Hello: ${Thread.currentThread().name}") //协程被延迟了1秒,但是主线程继续执行
}

日志打印:
2020-08-28 11:10:48.416 3008-3008/? D/zwm: Hello: main
2020-08-28 11:10:49.430 3008-29176/com.tomorrow.kotlindemo D/zwm: World: DefaultDispatcher-worker-1

delay只能在协程内部使用,它用于挂起协程,不会阻塞线程。

runBlocking:

fun method() { //线程
    runBlocking { //主协程
        launch { //子协程
            Log.d("TAG", "zwm, launch before delay")
            delay(1000L)
            Log.d("TAG", "zwm, launch after delay")
        }
        Log.d("TAG", "zwm, runBlocking before delay")
        delay(2000L)
        Log.d("TAG", "zwm, runBlocking after delay")
    }
    Log.d("TAG", "zwm, method")
}

日志打印:
2020-08-28 11:36:40.818 32645-32645/com.tomorrow.kotlindemo D/TAG: zwm, runBlocking before delay
2020-08-28 11:36:40.824 32645-32645/com.tomorrow.kotlindemo D/TAG: zwm, launch before delay
2020-08-28 11:36:41.828 32645-32645/com.tomorrow.kotlindemo D/TAG: zwm, launch after delay
2020-08-28 11:36:42.821 32645-32645/com.tomorrow.kotlindemo D/TAG: zwm, runBlocking after delay
2020-08-28 11:36:42.823 32645-32645/com.tomorrow.kotlindemo D/TAG: zwm, method

launch与runBlocking这两个函数都会启动一个协程,不同的是,runBlocking为最高级的协程,也就是主协程,launch创建的协程能够在runBlocking中运行(反过来是不行的)。需要注意的是,runBlocking方法仍旧会阻塞当前执行的线程。

join:

fun method() {
    runBlocking {
        val job = launch {
            search()
        }
        Log.d("TAG", "zwm, Hello")
        job.join()
    }
    Log.d("TAG", "zwm, method")
}

suspend fun search() {
    delay(1000L)
    Log.d("TAG", "zwm, search")
}

日志打印:
2020-08-28 13:11:37.109 815-815/com.tomorrow.kotlindemo D/TAG: zwm, Hello
2020-08-28 13:11:38.115 815-815/com.tomorrow.kotlindemo D/TAG: zwm, search
2020-08-28 13:11:38.117 815-815/com.tomorrow.kotlindemo D/TAG: zwm, method

用suspend修饰的方法在协程内部使用的时候和普通方法没什么区别,不同的是在suspend修饰的方法内部还可以调用其他suspend方法。比如,在上面的search方法中调用的delay就是一个suspend修饰的方法,这些方法只能在协程内部或者其他suspend方法中执行,不能在普通的方法中执行。

用同步方式写异步代码

suspend fun searchItemOne(): String {
    delay(1000L)
    return "item-one"
}

suspend fun searchItemTwo(): String {
    delay(1000L)
    return "item-two"
}

fun method() {
    runBlocking<Unit> {
        val one = searchItemOne()
        val two = searchItemTwo()
        Log.d("TAG", "zwm, items is $one and $two")
    }
    Log.d("TAG", "zwm, method")
}

日志打印:
2020-08-28 13:27:29.069 1121-1121/com.tomorrow.kotlindemo D/TAG: zwm, items is item-one and item-two
2020-08-28 13:27:29.069 1121-1121/com.tomorrow.kotlindemo D/TAG: zwm, method

使用async与await:

suspend fun searchItemOne(): String {
    delay(1000L)
    return "item-one"
}

suspend fun searchItemTwo(): String {
    delay(1000L)
    return "item-two"
}

fun method() {
    runBlocking<Unit> {
        val one = async { searchItemOne() }
        val two = async { searchItemTwo() }
        Log.d("TAG", "zwm, items is ${one.await()} and ${two.await()}")
    }
    Log.d("TAG", "zwm, method")
}

日志打印:
2020-08-28 13:32:51.028 8271-8271/com.tomorrow.kotlindemo D/TAG: zwm, items is item-one and item-two
2020-08-28 13:32:51.028 8271-8271/com.tomorrow.kotlindemo D/TAG: zwm, method

使用async也相当于创建了一个子协程,它会和其他子协程一样并行工作,与launch不同的是,async会返回一个Deferred对象。Deferred值是一个非阻塞可取消的future,它是一带有结果的job。launch会返回一个job对象,但是没有返回值。利用await方法可以等待获取到值之后,将值取出来。

2.共享资源控制

锁模式

虽然Kotlin是基于Java改良过来的语言,但是它没有synchronized关键字,取而代之,它使用了@Synchronized注解和synchronized()来实现等同的效果:

class Shop {
    val goods = hashMapOf<Long, Int>()

    init {
        goods[1] = 10
        goods[2] = 15
    }

    @Synchronized fun buyGoods(id: Long) {
        val stock = goods.getValue(id)
        goods[id] = stock - 1
    }

    fun buyGoods2(id: Long) {
        synchronized(this) {
            val stock = goods.getValue(id)
            goods[id] = stock - 1
        }
    }
}

Java中的volatile关键字在Kotlin中也变成了注解@Volatile:

@Volatile private var running = false

还可以使用Lock的方式来对代码进行加锁:

class Shop {
    private val goods = hashMapOf<Long, Int>()
    private val lock: Lock = ReentrantLock()

    init {
        goods[1] = 10
        goods[2] = 15
    }

    fun buyGoods(id: Long) {
        lock.lock()
        try {
            val stock = goods.getValue(id)
            goods[id] = stock - 1
        } catch (ex: Exception) {
            Log.d("TAG", "zwm, exception: $ex")
        } finally {
            lock.unlock()
        }
    }
}

对Lock方式进行改进:

class Shop {
    private val goods = hashMapOf<Long, Int>()
    private val lock: Lock = ReentrantLock()

    init {
        goods[1] = 10
        goods[2] = 15
    }

    fun buyGoods(id: Long) {
        lock.withLock {
            val stock = goods.getValue(id)
            goods[id] = stock - 1
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读