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 能够用作空调集的初始化。

委托/署理

官方文档:kotlinlang.org/docs/delega…

署理形式在 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.SYNCHRONIZED。

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代替了,详细可参阅这篇帖子:Why Kotlin decompiler generates null.INSTANCE。

这个 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<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<V> 为 Comparable<T> 的子类型,
而不是对泛型类型做的约束,因而 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)
    }
}