Kotlin 中的一些冷知识

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 实际上是在访问 ExampleUnitLazyTest$str$2.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 不是 List 的子类,该性质叫不变

协变

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

对于常用的集合类 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 是 Generic 的子类型,那么 Generic 可以称之为一个逆变类。

在 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 声明描述
ColllectionColllection不变
? extends Aout A协变,上界通配,生产者
? super Ain A逆变,下界通配,消费者
?*协变但上界为 Any?,通配符,等价与 out Any?
T extends AT : 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)
    }
}

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
img
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓

PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题

图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值