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);
}
}
可以看到核心可以看做:
- 在宿主的构造函数中创建 Lazy 示例,并将初始化函数封装传入。
- 创建对应属性的 get 方法,get 方法的实现是将 Lazy 对象的 get 方法返回(代理)。
- 再结合上述 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 实际上是在访问 ExampleUnitLazyTest2.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
通过打印的结果可以得到二者的主要区别:
- observable 闭包需返回空,而 vetoable 要求返回一个布尔值,顾名思义这个返回值决定了本次值设置是否生效。
- 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中的代理特点:
- Kotlin 支持属性和类的代理。
- 通过 by 关键字声明代理,并且其后必须跟一个具体对象。
- by 关键字后可以支持:
- Lazy 类型的对象,典型的 by viewModels
- lazy + 闭包,用于属性的延迟初始化,最后一行返回初始值。
- Delegates 相关API,用于 var 类的属性代理,可以在属性变更前后额外做一些事情
- 通过 ReadWriteProperty、ReadOnlyProperty 实现更一般的属性代理。
- 通过 :: 关键字 ,使用另一个属性作为代理。
- 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)
}
}