美文网首页
Kotlin 中的一些冷知识

Kotlin 中的一些冷知识

作者: BlueSocks | 来源:发表于2023-08-17 13:42 被阅读0次

    Kotlin 语言比 Java 更简洁、更易用,本文尝试从 Kotlin 中的部分新特性出发,了解一些我们常用,但又不太熟悉背后原理的一些知识盲区的: Unit 类、Nothing 类的特殊性、Kotlin 里的委托机制和泛型体系。

    Unit 类

    先来看 Unit.kt 的源码

    public object Unit {
        override fun toString() = "kotlin.Unit"
    }
    
    

    Unit 为单例类,在 Kotlin 中单例类既可以当一种类型,也可当一个对象,所以下面的例子是合法的,只不过单例对象可以直接访问,无需这样多次一举。

    //合法(但没用)
    val param : Unit = Unit
    
    

    函数的默认返回值的类型

    当做类型时使用时,Unit 为函数的默认返回值的类型。

    这里需要指出的是,与 Java 不同,Kotlin 中的函数都是有返回值的,只不过在不显式声明时默认为 Unit。

    fun foo() {}
    
    println(foo()::class)
    //class kotlin.Unit
    
    

    这样设计的好处是 Kotlin 中做到了一个统一,即所有函数都有返回值。

    但这种统一又有什么用呢?来看下面这个例子:

    // Java
    interface Factory {
        Object create();
    }
    
    class UselessFactory implements Factory {
        //非法 返回类型不能为空
        @Override
        public void create() {
        }
    }
    
    

    所以你不得不这样做,来表示没有返回值这件事

    class UselessFactory implements Factory {
        @Override
        public Object create() {
            return null //返回空
        }
    }
    
    

    但是在 Kotlin 中可以这样实现:

    // Kotlin
    class Toy
    interface Factory {
        fun create(): Any
    }
    
    class ToyFactory : Factory {
        override fun create(): Toy {
            return Toy()
        }
    }
    
    class UselessFactory : Factory {
        //合法(但没用)
        override fun create() {
        }
    }
    
    

    同样的返回值问题,在泛型场景中也同样存在,只不过为了补这个窟窿,Java中有专门的 Void 对象可以作为“没有返回值”函数的返回值类型。这里的 Void 与 Unit 作用是一样的。

    当做普通的单例对象使用

    最后,当把 Unit 当做一个单例对象时,可以用于一些无需特定含义的场景,只需要一个“现成的”对象和类型而已,如 LiveData 发出一个事件。

    //播放器底层发出一个buffer事件
    val loadingEvent = MutableLiveData<Unit>
    liveData.value = Unit
    
    

    Nothing 类

    来看源码:

    public class Nothing private constructor()
    
    

    通过源码可以看到 Nothing 构造器为私有,这表示它永远无法创建对象。 对于一个类型而言无法创建对象还有什么用呢?

    永远抛出异常的函数标志

    既然无法创建对象,那还当类型使用,比如可以用于一个永远抛出异常的方法的返回值:

    fun throwException(msg: String) : Nothing {
      throw RuntimeException(msg)
    }
    
    

    但这里的问题是既然总会抛出异常,那返回值还有什么意义呢?是的,这里的返回值类型可以是 String 或者其他类型,甚至直接不写。

    fun throwException(msg: String) {
      throw RuntimeException(msg)
    }
    
    

    所以那直接不写不就好了,为啥还要显式声明一个类型呢? 对,确实可以不写,这里最大的好处是可以提示函数的调用者,只要看到这个返回值类型,就能明白这个函数一定是以异常结束,仅此而已。

    这样的写法在 Kotlin 标准库非常常见,比如 TODO 函数,对你没看错,Kotlin 中 TODO 是用函数实现的。

    //Standard.kt
    public inline fun TODO(): Nothing = throw NotImplementedError()
    
    

    容器泛型类的默认占位类型

    在 Kotlin 中 Nothing 类型是所有类型的子类型,看下面这个例子

    val nothing: Nothing = TODO()
    //unreachable code
    //但可以将一个 Nothing 类型的变量赋值给任意对象
    var p: Person = nothing
    
    

    虽然 JVM 不支持多继承,但由于 Nothing 并不能创建任何具体的对象,所以并不会产生任何实质影响。

    借用这个特性可以将 Nothing 泛型容器赋值给任何其他类型,来看下面的例子。

    val emptyList: List<Nothing> = listOf()
    //合法
    var persons: List<Person> = emptyList
    //合法
    var cars: List<Car> = emptyList
    
    

    这里的 listof 函数返回一个 EmptyList 对象。

    // kotlin.collections
    internal object EmptyList : List<Nothing> {
        ...
    }
    
    

    由于这个 EmptyList 是一个单例对象,这样就能作为全局的空集合对象初始化使用,既方便又没有额外内存开销。

    总结一下就是 Nothing 可以用作空集合的初始化。

    委托/代理

    官方文档:https://kotlinlang.org/docs/delegation.html
    代理模式在 java 中是一种常见的设计模式,但是为了实现一套代理模式,我们不得不写大量的样板代码,看下面这个静态代理的例子:

    interface Base {
        fun printMessage()
        fun printMessageLine()
    }
    
    class Impl : Base {
        override fun printMessage() {
            print("impl print msg")
        }
    
        override fun printMessageLine() {
            println("impl println msg")
        }
    }
    
    //静态代理类
    class Proxy(private val origin: Base) : Base {
        override fun printMessage() {
            //do something special
            origin.printMessage()
        }
    
        override fun printMessageLine() {
            //do something special
            origin.printMessageLine()
        }
    }
    
    
    

    可以看到想要一个简单的静态代理,不得不复现所有接口方案,而实现都是简单的调用代理对象的对应方法。

    Kotlin 语言对代理模式实现了更简洁的支持。

    接口代理

    kotlin 提供了一个 by 关键字来消除这些样板代码:

    class Proxy(private val origin: Base): Base by origin {
        override fun printMessage() {
            //do something special
            origin.printMessage()
        }
    
        override fun printMessageLine() {
            //do something special
            origin.printMessageLine()
        }
    }
    
    

    你确定代码被简化了?明明还多出了 by origin!!

    是的,可以这是你需要代理并做一些额外处理的做法,如果你仅仅是想用一个代理对象,你的写法就简化为下面这样:

    class Proxy(private val origin: Base): Base by origin
    
    

    也就是说如果不显示声明复写接口的抽象方法,Kotlin 会默认为你加上上面例子中的模板代码。

    试想一下,如果一个代理接口有 n 多个方法,而我们实际可能只是需要对一个方法进行代理,Kotlin 将会减少大量的样板代码。

    这里需要额外注意的是 by 后面跟的必须返回一个具体的对象而不是类型,也可以是表达式,因此看到 by 关键字就可以将类型声明的前后隔开,无论声明多么复杂。

    class Proxy(private val origin: Base) : Base by
        if (BuildConfig.DEBUG) origin else originRelease
    
    

    最后需要指出的是同 java 的代理模式一样,kotlin 的代理模式仅支持接口类型,这本质上还是因为 JVM 不支持多继承的限制。

    Kotlin 还支持代理成员变量,因为在 Kotlin 中接口的成员变量也会转换为对应的 get 方法实现。

    属性代理

    属性代理是更为常见的使用场景,我们常用的 by lazy 语法延迟初始化的对象就是一种属性代理。

    常见的两种写法:

    //延迟创建vm对象
    private val vm by viewModels<MediaViewModel>()
    
    

    或者可以使用闭包通过一个函数返回延迟创建的对象。

    val api by lazy {
        ApiServiceManager.getContentApiService(NetConfigApi::class.java, DOMAIN)
    }
    
    

    其实二者的本质是一样的,本质上都是要求 by 关键字后返回一个 Lazy 对象,viewModels 和 lazy 都是函数,而这个函数的调用时机是在第一次访问该属性时。

    //LazyJVM.kt 源码
    public interface Lazy<out T> {
        public val value: T
        public fun isInitialized(): Boolean
    }
    
    

    lazy 属性

    lazy 函数为 Kotlin 标准包的内置函数:

    public actual fun <T> lazy(lock: Any?, initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)
    
    

    同时为处理多线程初始化的问题,还提供一个多参的函数:

    public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> 
    
    

    LazyThreadSafetyMode 提供三种多线程交互模式:

    模式 描述
    LazyThreadSafetyMode.SYNCHRONIZED 线程安全,仅有一个线程可以执行初始化函数,初始化阶段其他线程访问变量会阻塞。
    LazyThreadSafetyMode.PUBLICATION 初始化函数可能执行多次,最早执行完的函数作为属性的最终值。
    LazyThreadSafetyMode.NONE 默认选项,初始化函数可能执行多次,每个线程都得到一个实例的值。

    在上面的简单示例中未指定模式则默认为 LazyThreadSafetyMode.NONE,性能更好。

    Lazy 是如何工作的?

    无论使用上述的那种线程模式,总得原则没变,那就是被 lazy 声明的属性会在首次访问时初始化,初始化赋值结束后访问该属性都是读取的缓存值。

    结合上面 Lazy 接口的我们可以这样理解 Lazy 实现类的内部逻辑:

    //伪代码
    class XxxLazyImpl<out T>(initializer: () -> T) : Lazy<T> {
        private var _value: Any? = UNINITIALIZED_VALUE
        override val value: T
            get() {
                val _v1 = _value
                if (_v1 !== UNINITIALIZED_VALUE) {
                    return _v1
                }
    
                //A 执行初始化函数
                val typedValue = initializer!!()
                _value = typedValue
                return _value
            }
    }
    
    

    不同的线程模式,也只是在 A 点有不同的锁处理而已,读者可自行参考源码。

    至于 Lazy 在宿主的实现可以结合下面的例子理解:

    class ExampleUnitLazyTest {
        val str: String by lazy {
            "Hello Lazy"
        }
    
        fun printStr() {
            println("str:$str")
        }
    }
    
    

    Kotlin 代码经 decompile 之后的结果如下:

    public final class ExampleUnitLazyTest {
       private final Lazy str$delegate;
    
       public final String getStr() {
          Lazy var1 = this.str$delegate;
          Object var3 = null;
          return (String)var1.getValue();
       }
    
       public final void printStr() {
          String var1 = "str:" + this.getStr();
          System.out.println(var1);
       }
    
       public ExampleUnitLazyTest() {
          this.str$delegate = LazyKt.lazy((Function0)null.INSTANCE);
       }
    }
    
    

    可以看到核心可以看做:

    1. 在宿主的构造函数中创建 Lazy 示例,并将初始化函数封装传入。
    2. 创建对应属性的 get 方法,get 方法的实现是将 Lazy 对象的 get 方法返回(代理)。
    3. 再结合上述 Lazy 内部初始化逻辑将整体链路串联。

    在上面代码中出现的 null.INSTANCE 是由于kotlin 反编译器不能识别自动生成的类,所以用null代替了

    这个 Lambda 背后隐藏的类,经字节码解析后,大概会是下面这个样子:

    //synthetic class
    class com/bytedance/auto/testkotlin/ExampleUnitLazyTest$str$2 extend Lambda implements Function0 {
    
        public final static ExampleUnitLazyTest$str$2 INSTANCE;
    
        static {
            INSTANCE = ExampleUnitLazyTest$str$2()
        }
        
        public bridge Object invoke() {
            return invoke()
        }
        
        public final String invoke() {
            return "Hello Lazy"
        }
        
        ExampleUnitLazyTest$str$2() {
            Lamada(0)
        }
        
    }
    
    

    最后上面 null.INSTANCE 实际上是在访问 ExampleUnitLazyTeststr2.INSTANCE

    Delegates API

    除了 lazy 相关语法,Kotlin 还支持 Delegates 相关 API 做属性代理,用于变量变化前后做一些额外的事情,核心的两个 API 为: Delegates.vetoable vs. Delegates.observable。

    var name: String by Delegates.observable("init") { prop, old, new ->
            println("name exe $name")
            println("$old -> $new")
        }
    
    var age: Int by Delegates.vetoable(10) { prop, old, new ->
        println("age exe $name")
        println("$old -> $new")
        old < new
    }
    
    @Test
    fun testObservable() {
        name = "zhangsan"
        println("name is $name")
        println("-----------")
        age = 20
        println("age is $age")
        println("-----------")
        age = 18
        println("age is $age")
    }
    
    
    

    执行的结果为:

    name exe zhangsan
    init -> zhangsan
    name is zhangsan
    -----------
    age exe zhangsan
    10 -> 20
    age is 20
    -----------
    age exe zhangsan
    20 -> 18
    age is 20
    
    

    通过打印的结果可以得到二者的主要区别:

    1. observable 闭包需返回空,而 vetoable 要求返回一个布尔值,顾名思义这个返回值决定了本次值设置是否生效。
    2. observable 不能改变设置变量的结果,当回调 callback 闭包时已经将属性值改变了;而 vetoable 回调的闭包中还是原值。

    代理其他属性

    kotlin还提供双冒号::的语法,用于代理属性或方法。 对于 val 类型的属性,代理类需包含对应属性的 getter 方法;对于 var 类型的,必须同时包含 getter 和 setter。

    data class Animal(var weight: Int)
    
    private val animal = Animal(10)
    private var weight: Int by animal::weight
    
    @Test
    fun testDelegate() {
        println("weight: $weight")
        weight = 20
        println("animal weight: ${animal.weight}")
    }
    
    输出结果:
    weight: 10
    animal weight: 20
    
    

    如果代理类就是 this,可以省略:

    class MyClass {
       var newName: Int = 0
       @Deprecated("Use 'newName' instead", ReplaceWith("newName"))
       var oldName: Int by ::newName //省略this
    }
    
    

    代理map

    kotlin 内实现了对 Map 的代理,来看例子:

    class User(val map: Map<String, Any?>) {
        val name: String by map
        val age: Int     by map
    }
    
    val user = User(mapOf(
        "name" to "John Doe",
        "age"  to 25
    ))
    
    println(user.name) // Prints "John Doe"
    println(user.age)  // Prints 25
    
    

    通过打印结果可以看到被代理的 name/age 属性,相当于调用 map["name"]/map["age"]。

    对于 var 类型的属性,对应的可以使用 MutableMap 代理。

    更一般的属性代理方式

    事实上,Kotlin支持更一般的属性代理方法,如果我们在by关键字后随便声明一个对象则会收到这样的提示。

    class ResourceDelegate
    
    class Owner {
        var varResource: Resource by ResourceDelegate() //compile error
    }
    
    //Type 'ResourceDelegate' has no method 'getValue(Owner, KProperty<*>)' and thus it cannot serve as a delegate
    //Type 'ResourceDelegate' has no method 'setValue(Owner, KProperty<*>, Resource)' and thus it cannot serve as a delegate for var (read-write property)
    
    

    当我们按报错要求补充对应 getValue 和 setValue 后报错消失。

    class ResourceDelegate(private var resource: Resource = Resource()) {
        operator fun getValue(thisRef: Owner, property: KProperty<*>): Resource {
            return resource
        }
    
        operator fun setValue(thisRef: Owner, property: KProperty<*>, value: Any?) {
            if (value is Resource) {
                resource = value
            }
        }
    }
    
    

    一般的,对于 var 类型的属性,代理对象需提供 getValue 和 setValue 两个方法,而 val 类型的带来,只需提供 getValue 方法。

    Kotlin 提供了相应的 ReadWriteProperty、ReadOnlyProperty 实现了模板代码的封装,上面的例子可以改写成:

    fun resourceDelegate(resource: Resource = Resource()): ReadWriteProperty<Any?, Resource> =
        object : ReadWriteProperty<Any?, Resource> {
            var curValue = resource
            override fun getValue(thisRef: Any?, property: KProperty<*>): Resource = curValue
            override fun setValue(thisRef: Any?, property: KProperty<*>, value: Resource) {
                curValue = value
            }
        }
    
    val readOnlyResource: Resource by resourceDelegate()
    var readWriteResource: Resource by resourceDelegate()
    
    

    代理总结

    总结一下kotlin中的代理特点:

    1. Kotlin 支持属性和类的代理。
    2. 通过 by 关键字声明代理,并且其后必须跟一个具体对象。
    3. by 关键字后可以支持:
      1. Lazy 类型的对象,典型的 by viewModels
      2. lazy + 闭包,用于属性的延迟初始化,最后一行返回初始值。
      3. Delegates 相关API,用于 var 类的属性代理,可以在属性变更前后额外做一些事情
      4. 通过 ReadWriteProperty、ReadOnlyProperty 实现更一般的属性代理。
      5. 通过 :: 关键字 ,使用另一个属性作为代理。
      6. Map 类型特定的代理方式。

    泛型

    要讲清楚 kotlin 中的泛型,还是需要先回顾 java 中的型变,它包括:不变、协变、逆变。

    不变 invariant

    来看一个例子:

    // Java
    List<String> strs = new ArrayList<String>();
    List<Object> objs = strs; // !!! A compile-time error here saves us from a runtime exception later.
    objs.add(1); // Put an Integer into a list of Strings
    String s = strs.get(0); // !!! ClassCastException: Cannot cast Integer to String
    
    

    可以看到,如果不对 List<Object> objs = strs这个赋值动作做限制,将会出现不可预期的运行时错误,而这与泛型设计的理念不符。

    在例子中 List<String> 不是 List<Object> 的子类,该性质叫不变

    协变

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

    对于常用的集合类 Collect,假设我们考虑实现一个 addAll 接口,用于批量增加元素,按下面的代码:

    interface Collection<E> ... {
        void addAll(Collection<E> items);
    }
    
    

    由于默认不变的性质,下面的代码将编译失败:

    void copyAll(Collection<Object> to, Collection<String> from) {
        to.addAll(from);
        // Collection<String> is not a subtype of Collection<Object>
    }
    
    

    为了解决这个问题,引入的上界通配,该性质叫协变

    interface Collection<E> ... {
        void addAll(Collection<? extends E> items);
    }
    
    

    Collection<? extends E> 确保了集合中元素均为 E 或其子类,那么把这样一个元素加入到 Collection 类型的集合中一定没问题。

    同时 Collection<? extends E> 类型的集合不允许添加元素,因为一旦允许添加元素,就会存在不变场景的 case。

    因此,可以总结协变场景下,只能读(取出)不能写,读取会返回一个协变上界类型的对象,也可以叫做生产者模式。

    生产者表示只能往外读取数据 T,而不从中添加数据。消费者表示只往里插入数据 T,而不读取数据。

    在 Kotlin 中使用 out E 替代 ? extends E,并且使用了 out 声明的泛型,该泛型只能用于方法的返回中,举个例子:

    interface Source<out T> {
        fun nextT(): T
    }
    
    

    回过头来,上面不变的例子使用 Kotlin 语言会发生什么呢?

    //kotlin
    val strs: List<String> = ArrayList()
    val objs: List<Any> = strs // OK!
    
    
    

    可以看到,在 Kotlin 中的 List 也是协变的,这是因为这里的 List 是 Kotlin 基础包中的 List,其中对泛型做了协变声明:

    package kotlin.collections
    
    public interface List<out E> : Collection<E> {
        ...
    }
    
    

    但是对于 Kotlin 中的 ArrayList 来说还是不变的。

    @SinceKotlin("1.1") public actual typealias ArrayList<E> = java.util.ArrayList<E>
    
    
    

    逆变

    与协变相反,如果只可能以入参的形式使用泛型,则可以使用逆变,对于支持逆变的集合只能向其中添加数据而不能读取。

    由于 Kotlin 中的 List 接口本身不支持 add,我们以java中的List举例:

    List<? super Animal> animals = new ArrayList<>();
    animals.add(new Dog()); //OK
    
    

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

    在 Kotlin 中使用 in E 替代 ? super E

    下面是一个 Kotlin 版本逆变的例子:

    interface Comparable<in T> {
        operator fun compareTo(other: T): Int
    }
    
    fun demo(x: Comparable<Number>) {
        x.compareTo(1.0) // 1.0 has type Double, which is a subtype of Number
        // Thus, you can assign x to a variable of type Comparable<Double>
        val y: Comparable<Double> = x // OK!
    }
    
    

    这里需要强调的是,逆变是限制泛型类的父子类关系,而不是泛型类型本身, 上面的例子中声明 Comparable 泛型类使用 T 的逆变类型,意思是 对于 T 的任何父类 V, Comparable 为 Comparable 的子类型, 而不是对泛型类型做的限制,因此 x.compareTo(1.0) 是合法的。

    其他通配符和泛型上下界不再一一举例,以下表格为 Kotlin 和 Java 的对应关系,其中的 A 为具体类型,T 为泛型占位符。

    java 声明 kotlin 声明 描述
    Colllection<A> Colllection<A> 不变
    ? extends A out A 协变,上界通配,生产者
    ? super A in A 逆变,下界通配,消费者
    ? * 协变但上界为 Any?,通配符,等价与 out Any?
    T extends A T : A 不变,泛型上界

    reified 关键字

    泛型的出现本身是为了保证在编译期检查出更多错误,避免在运行期发生异常。而由于 JDK 从 1.5 版本开始才支持泛型特性,为兼容老版本 JDK,引入了泛型擦除的概念,这使得在开发中我们不能把泛型当做真实的类型使用:

    //java
    public <T> void isString(T input) {
        if (T instanceof String) { // compile error
        }
    }
    
    

    为解决这个问题,不得不要求方法入参再添加一个Class类型的参数。

    public <T> void isString(Object input, Class<T> type) {
        if (type.isInstance(input)) { // OK!
        }
    }
    
    

    像这种获取具体的泛型类型的需求,在Kotlin有了更友好的实现,那就是在泛型类型前使用 reified 关键字,上面的例子可以简化为:

    inline fun <reified T> isString(input: T) {
        if (input is String) { // OK!
        }
    }
    
    

    这个特性在反序列化场景非常实用:

    inline fun <reified T> String?.toObject(type: Type? = null): T? {
        return if (type != null) {
            GsonFactory.GSON.fromJson(this, type)
        } else {
            GsonFactory.GSON.fromJson(this, T::class.java)
        }
    }
    
    

    相关文章

      网友评论

          本文标题:Kotlin 中的一些冷知识

          本文链接:https://www.haomeiwen.com/subject/akxrmdtx.html