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);
}
}
能够看到核心能够看做:
- 在宿主的结构函数中创立 Lazy 示例,并将初始化函数封装传入。
- 创立对应特色的 get 办法,get 办法的完成是将 Lazy 目标的 get 办法回来(署理)。
- 再结合上述 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
经过打印的成果能够得到二者的首要差异:
- 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<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)
}
}