之前就写过一篇泛型的文章,可是总觉得写得不够体系,所以最近对泛型又作了些研究,算是对这篇文章的弥补了。
kotlin修炼指南7之泛型
泛型,是为了让「类」、「接口」、「办法」具有愈加通用的运用范围而诞生的,举个比如,假如咱们不运用泛型,那么一个List中能够装得下任何目标,这么做的问题就在于,在运用时,需要对类型进行检查,不然就会转化反常。
所以,咱们需要将这种检查前置到编译期,这样在编写代码时,就能够安全的运用不同类型,例如List,咱们一看就知道是一个String类型的list,不能放其他类型的元素。
在Java中,由于前史原因,它并不存在真泛型,Java一切的泛型都是伪泛型,由于Java在编译期,会执行「泛型擦除」,然后导致在Java字节码中,不存在类型信息(可是类型会被保存在其它地方,这个后边讲)。
正是由于泛型擦除的问题,你乃至能够经过反射绕开泛型的束缚,传递一个非当时泛型束缚的目标。
泛型类型在Java中,一般以一个大写字母来进行标识,咱们并不是一定要写「T」来表明泛型,但这是一个约好成俗的表明,相似的束缚还有下面这些。
- 通用泛型类型:T,S,U,V
- 调集元素泛型类型:E
- 映射键-值泛型类型:K,V
- 数值泛型类型:N
要理解Kotlin的泛型,咱们最好首要从Java的泛型来学习,毕竟Kotlin的语法糖太多了,Java会愈加白话文一点。
首要,Java中的泛型具有「不变性」,也便是说,编译器会以为List和List是两个完全不同的类型,当然,不只仅是List,比方下面这个比如。
open class A
class B : A()
那么Test<A>和Test<B>是不是一个类型呢?有必要不是,尽管A和B是父子联系,但Test<A>和Test<B>就不是了,为什么呢?咱们站在编译器的视点来想想,假如它们是同一个类型,那么在Test类中get出来的实例,到底是A仍是B呢?所以编译器为了避免这种不确定性,就否定了Test<A>和Test<B>是一种类型的推断。
可是这种处理在咱们处理泛型业务时,会有许多束缚,所以,泛型供给了「型变」来拓展泛型的运用。
协变
协变指的是,当参数具有父子联系时,子类能够作为参数传递,而泛型的上界便是其父类。协变经过上界通配符<? extends 父类型>来完成。
实例化时可确定为「父类型的未知类型」,所以它「只能读不能写」,由于编译器不确定到底是哪个子类。
例如下面的代码。
List<Button> buttons = new ArrayList<Button>();
List<? extends TextView> textViews = buttons;
由于Button是TextView的子类,所以上面的代码能够正确运转。咱们来解释下上面的代码。
- 「?」通配符表明这是一个未知类型
- 「extends」上界通配符表明这个类型只能是其子类或许自身
- 这儿不只能够是类,也能够适用于接口
上界通配符还有一个特例,那便是「?」,例如List<?>,实践上便是List<? extends Object>的缩写。
在Kotlin中,运用的是「*」,即List<*>,实践上便是List<out Any>
简而言之,协变便是——假如A是B的子类,那么Generic<A>便是Generic<? extends B>的子类型。
协变的束缚
咱们来看下面的代码。
List<? extends TextView> textViews = new ArrayList<TextView>();
TextView textView = textViews.get(0);
// Error
textViews.add(textView);
�咱们来解释下上面的代码,首要,咱们界说了一个具有泛型上界的list,然后,咱们从list中读取一个元素,这时分,这个元素的回来类型是什么呢?编译器并不知道,但由于泛型上限的存在,所以它一定是TextView及其子类,所以界说为TextView类型,也完全没有问题。
接下来咱们来完成写入,这时分,就报错了。看上去如同没错啊,add进去的元素是TextView类型,契合泛型上界的界说啊,可是,这个List的类型界说是<? extends TextView>,编译器并不知道详细是什么类型,所以它就以为,最好的办法便是什么都不让加,多做便是错,那不如不做。
所以,经过协变之后的泛型,就失去了写入的才干,它只能用于向外供给数据,也便是「数据生产者Producer」。
逆变
逆变指的是,父类能够作为参数传递,但子类有必要是其下界。逆变经过下界通配符<? super 子类型>来完成。
实例化时可确定为「子类型的未知类型」,所以「只能写不能读」。
不能读指的是不能读取为指定的类型,而不是不能调用读的办法。
例如下面的代码。
List<? super Button> buttons = new ArrayList<TextView>();
相同咱们来分析下上面的代码。
- 「?」通配符表明这是一个未知类型
- 「super」下界通配符表明后边的这个类型,只能是它子类或许自身
- 这儿不只能够是类,也能够适用于接口
其实这整个便是协变的反向操作。一个是束缚上界,另一个是束缚下界,所以对比着,其实很好理解。
简而言之,逆变便是——假如A是B的子类,那么Generic<B>便是Generic<? super A>的子类型。
逆变的束缚
相似的,咱们再来看下逆变的束缚。
List<? super Button> buttons = new ArrayList<TextView>();
Button button = new Button(context);
buttons.add(button);
Object object = buttons.get(0);
上面的代码,创立了一个�list,它的元素类型的下界是Button,也便是说,这个list里边都是放的Button的父类类型。
所以,当咱们创立一个Button,并写入的时分,是完全能够的,由于它契合咱们界说下界的束缚。
再来看看读取呢?当咱们从list中读取一个元素时,由于编译器只知道它是Button的父类,可是详细是什么类型,它也不知道,所以,编译器不如将它作为Object这个万物基类了。
所以说,逆变之后的泛型,失去了读的才干(由于读出来都是Object),所以逆变泛型一般都作为「数据顾客Consumer」。
Kotlin型变
泛型让咱们有了能够支撑多种类型的才干,型变让咱们有了修改泛型的才干,总结来说:
- 泛型通配符<? extends x>能够使泛型支撑协变,可是「只能读不能写」,这儿的写,指的是对泛型调集添加元素,假如是remove(int index)或许是clear这种删去,则不受影响。
- 泛型通配符<? super x>能够使泛型支撑逆变,可是「只能写不能读」,这儿的读,指的是不能依照泛型类型读,但假如依照Object读出来再强转详细类型,则是能够的。
在学习了Java泛型之后,咱们再来看下Kotlin的泛型,这时分你再看,就没那么复杂了,核心就两条。
- 运用关键字 out 来支撑协变,等同于 Java 中的上界通配符 ? extends
- 运用关键字 in 来支撑逆变,等同于 Java 中的下界通配符 ? super
其实在理解了逆变和协变之后,你会发现out和in这两个关键字真的是「言简意赅」,out表明输出,即协变只用于输出数据,in表明输入,即逆变只用于写入数据。Kotlin官网上有个著名的——Consumer in, Producer out,说的便是这个意思。
Kotlin泛型的优化
咱们经过这个比如来看下Kotlin对Java泛型的改进。
申明处型变
咱们经过下面这个比如来看下Kotlin申明处型变的好处,这是一个生产者与顾客的比如,代码如下。
// 生产者
class Producer<T> {
fun produce(): T {}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce()
首要咱们来看生产者,关于T类型的Producer,咱们要创立它的子类时,就需要运用协变,即Producer,不然它就只能生产Button类型的数据。所以,在Java中,每次获取数据的时分,都要声明一次协变,所以Kotlin对其进行了优化,能够在申明处进行协变,代码如下。
// 生产者
class Producer<out T> {
fun produce(): T {}
}
val producer1: Producer<TextView> = Producer<Button>()
val producer2: Producer<out TextView> = Producer<Button>()
Kotlin约好,当泛型参数T只会用来输出时,能够在申明类的时分,直接运用协变束缚,这样在调用的时分,就不必额定运用协变了,当然写了也不会错。
与此相似的,顾客也是如此。
// 顾客
class Consumer<T> {
fun consume(t: T) {}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context))
咱们在运用的时分,也是有必要运用逆变,借助Kotlin,相同能够在申明处进行逆变。
// 顾客
class Consumer<in T> {
fun consume(t: T) {}
}
val consumer1: Consumer<Button> = Consumer<TextView>()
val consumer2: Consumer<in Button> = Consumer<TextView>()
这样在调用的时分,就不必额定运用逆变了,当然写了也不会错。
reified
由于在Java会进行泛型擦除,所以编译器无法在运转时知道一个切当的泛型类型,也便是说,咱们无法在运转时,判别一个目标是否为一个泛型T的实例,例如下面的代码。
if (item instanceof T) {
System.out.println(item);
}
相同的,在Kotlin里边也是不可的,毕竟一母同胞。
if (item is T) {
println(item)
}
为了解决这个问题,在Java或许Kotlin中,咱们一般会多传入一个Class类型的参数,然后经过Class.isInstance来判别类型是否匹配。
可是由于Kotlin支撑了内联函数,所以它供给了一个愈加便利的办法来处理这种场景,那便是「reified」合作「inline」来完成。
inline fun <reified T> checkType(item: Any) {
if (item is T) {
println(item)
}
}
不是说好了不能直接对泛型来做类型判别吗,为什么这儿却能够呢?这其实便是内联的作用,尽管这儿是对T做判别,但实践上在编译时,这儿现已被替换成了详细的类型,而不再是泛型T了,所以当然能够运用is来进行类型判别了。
支撑协变的List
在Kotlin中,有两种List,一种是可变的,一种是不可变的,即MutableList和List,其中List的申明如下,它现已完成的协变,所以Kotlin中的List只能读而不能写。
public interface List<out E> : Collection<E>
获取泛型的详细类型
reified
经过reified和inline合作,咱们能够在运转时获取泛型的详细类型,这是Kotlin的特性,详细的运用办法,上面的文章现已讲了一个比如。下面咱们再看看几个比较典型的比如。
fun reifiedClass() {
// normal
val serviceImpl1 = ServiceLoader.load(Service::class.java)
// reified
val serviceImpl2 = loadService<Service>()
}
inline fun <reified T> loadService() {
ServiceLoader.load(T::class.java)
}
interface Service {
fun work()
}
再看一个简化startActivity的办法。
inline fun <reified T : Activity> Activity.startActivity(bundle: Bundle? = null) {
val intent = Intent(this, T::class.java)
bundle?.let {
intent.putExtras(it)
}
startActivity(intent)
}
startActivity<SampleActivity>()
传入指定Class
经过传入详细的Class类型,咱们也能够在运转时获取泛型类型,这个办法是Java和Kotlin都支撑的,这个在前面的文章中也提到了。
匿名内部类
匿名内部类会在运转时实例化,这个时分,就能够拿到泛型的详细类型了,示例代码如下。
open class Test<T>
fun main() {
val innerClass = object : Test<String>() {}
val genericType: Type? = innerClass.javaClass.genericSuperclass
if (genericType is ParameterizedType) {
val type = genericType.actualTypeArguments[0]
// class java.lang.String
}
}
Class类供给了一个办法getGenericSuperclass ,经过它能够获取到带泛型信息的父类Type(Java的Class文件会保留承继的父类或许接口的泛型信息)。
经过对获取的genericType来判别是否完成ParameterizedType接口,是阐明支撑泛型,然后获取出对应的泛型列表(由于泛型可能有多个)。
这个办法是一个很巧妙的获取泛型类型的办法,在Gson中,便是经过它来获取类型的。
val content = Gson().toJson("xxx", object : TypeToken<String>() {}.type)
在运用Gson时,咱们需要创立一个承继自TypeToken的匿名内部类, 并实例化泛型参数TypeToken,这样咱们就能够经过getGenericSuperclass来获取父类的Type,也便是上面比如中的TypeToken了。
反射
反射自然是能够拿到运转时的详细类型了,代码如下。
open class Test<T>
class NewTest : Test<String>() {
private val genericType: Type? = javaClass.genericSuperclass
fun test() {
if (genericType is ParameterizedType) {
val type = genericType.actualTypeArguments[0]
// class java.lang.String
}
}
}
经过反射来获取实践类型,是很大开源库中都在运用的办法,例如Retrofit,它在内部便是经过method.genericReturnType�来获取泛型的回来类型,经过method.genericParameterTypes�来获取泛型的参数类型。
不过这儿咱们要猎奇了,在文章的一开始,咱们就说了,Java的伪泛型,会在编译时进行泛型擦除,那么反射又是怎么拿到这些泛型信息的呢?
其实,编译器仍是留了一手,申明处的泛型信息,实践上会以Signature的形式,保存到Class文件的Constant pool中,这样经过反射,就能够拿到详细的泛型类型了。
要注意的是,这儿能保留的是申明处的泛型,假如是调用途的泛型,例如办法的传参,这种就不会被保存了。
PESC
PESC是泛型型变中的一个指导性原则,意为「Producer Extend Consumer Super」,当然在Kotlin中,这句话要改为「Consumer in, Producer out」。
这个原则是从调集的视点动身的,其目的是为了完成调集的多态。
- 假如仅仅从调集中读取数据,那么它便是个生产者,能够运用extend
- 假如仅仅往调集中增加数据,那么它便是个顾客,能够运用super
- 假如往调集中既存又取,那么你不应该用extend或许super
仍是举一个比如来阐明,咱们能够以为Kotlin是Java的子类,可是List和List却是两个无关的类,它们之间没有承继联系,而运用List<? extends Java>后,相当于List和List之间也有了承继联系,然后能够读取List中不同类型的数据,List便是经过这种办法来完成了调集的多态。
协变和逆变的运用场景
咱们来看这样一段代码,咱们创立了一个copyAll的办法,传入to和from两个列表,代码如下。
fun <T> copyAll(to: MutableList<T>, from: MutableList<T>) {
to.addAll(from)
}
fun main() {
val numberList = mutableListOf<Number>() // to
val intList = mutableListOf(1, 2, 3, 4) // from
copyAll(numberList, intList)// Error
}
可是这段代码是不能编译经过的,原因在于to是一个List,而from是一个List,所以类型转化反常,不能编译。
但实践上,咱们知道Int是能够转化为Number的,可是编译器不知道,所以它只能报错,编译器需要的,便是咱们告诉它,这样做是安全的,得到了咱们的确保,编译器才干执行编译。
这个确保是从两方面来说的,首要咱们来看from。
from是一个List,完全能够当做List,所以,要确保「from取出来的元素能够转为Number类型,并且from不能再有其它写入」,不然你向一个List中插入了一条Number类型的元素,那就不乱套了。
所以,咱们能够对from做协变,让它只读不写,代码如下。
fun <T> copyAll(to: MutableList<T>, from: MutableList<out T>) {
to.addAll(from)
}
这样就表明from,只承受T或许T的子类型,也便是说,from只能是Number或许Number的子类型,而此刻from是Int类型,所以编译经过了。
上面是从from的视点做的确保,那么从to方面呢?
关于to来说,咱们需要确保「to只能写入,而不能读取」。
fun <T> copyAll(to: MutableList<in T>, from: MutableList<T>) {
to.addAll(from)
}
这样就表明to,只承受T或许T的父类型,也便是说,to只能是Int或许Int的父类型,而此刻to是Number类型,所以编译经过了。
别的,咱们将from的签名改为List,也是能够编译的,其原因便是Kotlin中的List现已支撑协变了。
相信咱们经过这个比如,大概能理解协变和逆变的运用办法了。
那么咱们在实践的代码中,要在哪些场景运用协变和逆变呢?
一般来说,泛型参数协变后则表明——「这个参数在当时类中,只能作为函数的回来值,或许是只读特点」。
abstract class TestOut<out T> {
abstract val num: T// 只读特点
abstract fun getItem(): T// 函数的回来值
abstract var num1 : T// Error 用于可变特点
abstract fun addItem(t: T)// Error 用于函数的参数
}
而逆变,表明这个参数「只能作为函数的参数,或许润饰可变特点」。
abstract class TestIn<in T> {
abstract val num: T//Error 只读特点
abstract fun getItem(): T//Error 函数的回来值
abstract fun addItem(t: T)// 用于函数的参数
}