从Kotlin的in和out说起
提到Kotlin的泛型,通常离不开in和out关键字
下面这段Java代码在日常开发中应该很常见了:
List<TextView> textViews = new ArrayList<TextView>();
其中List<TextView表示这是一个泛型类型为TextView的List。
那到底什么是泛型呢?
现在的程序开发的大都是面向对象的,平时会用到各种类型的对象,一组对象通常要用集合来存储它们,因而就有了一些集合类,比如List、Map等。
这些集合类里面都是装的具体类型的对象,如果每个类型都去实现诸如TextViewList、ActivityList这样的具体的类型,显然是不可能的。
因此就诞生了「泛型」,它的意思是把具体的类型泛化,编码的时候用符号来指代类型,在使用的时候,再确定它的类型。
前面那个例子,List<TextView>就是泛型类型声明。
既然泛型是跟类型相关的,那么是不是也能使用类型的多态呢?
先看一个常见的使用场景:
TextView textView = new Button(context);
// 👆🏻这是多态
List<Button> buttons = new ArrayList<Button>();
List<TextView> textViews = button;
//👆🏻多态用在这里会报错,incompatible types: List<Button> cannot be converted to List<TextView>
我们知道Button是继承自TextView的,根据Java多态的特性,第一处赋值是正确的。
但是到了List<TextView>的时候IDE就报错了,这是因为Java的泛型本身具有「不可变性Invariance」,Java里面认为List<TextView>和List<Button>类型并不一致,也就是说,子类型的泛型(List<Button>)不属于泛型(List<TextView>)的子类。
Java的泛型类型会在编译时发生类型擦除,为了保证类型安全,不允许这样赋值。 但是换成数组就没有在编译时擦除类型。
但是在实际使用中,我们的确会有这种类似的需求,需要实现上面这种赋值。
Java提供了「泛型通配符」 ? extends和? super来解决这个问题。
Java中的? extends
在Java里面是这么解决的:
List<Button> buttons = new ArrayList<Button>();
List<? extends TextView> textViews = buttons;
这个? extends叫做「上界通配符」,可以使Java泛型具有「协变性Convariance」,协变就是允许上面的赋值是合理的。
在继承关系树中,子类继承自父类,可以认为父类在上,子类在下。`extends`限制了泛型类型的父类型,所以叫上界。
它有两层意思:
- 其中
?是个通配符,表示这个List的泛型类型是一个未知类型。 extends限制了这个未知类型的上界,也就是泛型类型必须满足这个extends的限制条件,这里和定义class的extends关键字有点不一样:- 它的范围不仅是所有直接或间接子类,还包括上界定义的父类本身,也就是
TextView. - 它还有
implements的意思,即这里的上界也可以是interface。
- 它的范围不仅是所有直接或间接子类,还包括上界定义的父类本身,也就是
这里Button是TextView的子类,满足了泛型类型的限制条件,因而能够成功赋值。
根据刚才的描述,下面几种情况都是可以的:
List<? extends TextView> textViews = new ArrayList<TextView>();// 本身
List<? extends TextView> textViews = new ArrayList<Button>();// 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>();//间接子类
一般集合类都包含了get和add两种操作,比如Java中的List,它的具体定义如下:
public interface List<E? extends Collection<E>{
E get(int index);
boolean add(E e);
...
}
上面的代码中,E就是表示泛型类型的符号(用其他字母甚至单词都可以).
我们看看在使用了上界通配符之后,List的使用上有没有什么问题:
List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0);// get可以
textViews.add(textView);
//add 会报错,no suitable method found for add(TextView)
前面说到List<? extends TextView>的泛型类型是个未知类型?,编辑器也不确定它是啥类型,只是有个限制条件。
由于它满足? extends TextView的限制条件,所以get出来的对象,肯定是TextView的子类,根据多态的特性,能够赋值给TextView,啰嗦一句,赋值给View也是没有问题的。
到add操作的时候,我们可以这么理解:
List<? extends TextView>由于类型未知,它可能是List<Button>,也可能是List<TextView>。- 对于前者,显然我们要添加TextView是不可以的。
- 实际情况是编辑器无法确定到底属于哪一种,无法继续执行下去,就报错了。
要我干脆不要extends TextView,只用通配符?呢?
这样使用List<?>其实是List<? exteds Object的缩写。
List<Button> buttons = new ArrayList<>();
List<?> list = buttons;
Object obj = list.get(0);
list.add(obj);这里还是会报错
和前面的例子一样,编辑器没法确定?的类型,所以这里就只能get到Object对象。
同时编辑器为了保证类型安全,也不能向List<?>中添加任何类型的对象,理由同上。
由于add的这个限制,使用了? extends泛型通配符List,只能够向外提供数据类型被消费,从这个角度来讲,向外提供数据的一方称为「生产者Producer」。对应的还有一个概念叫「消费者Consumer」,对应Java里面另一个泛型通配符? super。
Java中的?super
List<? super Button> buttons = new ArrayList<TextView>();
这的? super叫做「下界通配符」,可以使用Java泛型具有「逆变性Contravariance」.
与上界通配符对应,这里super限制了通配符?的子类型,所以称之为下界。
它也有两层意思:
- 通配符
?表示List的泛型类型是一个未知类型。 super限制了这个未知类型的下界,也就是泛型必须满足这个super的限制条件。super我们在类的方法里面经常用到,这里的范围不仅包括Button的直接和间接父类,也包括下界Button本身。super同样支持interface。
上面的例子中,TextView是Button的父类型,也就能够满足super的限制条件,就可以成功赋值了。
根据刚才的描述,下面几种情况都是可以的:
List<? super Button> buttons = new ArrayList<Button>();// 本身
List<? super Button> buttons = new ArrayList<TextView>();//直接父类
List<? super Button> buttons = new ArrayList<Object>();//间接父类
对于使用了下界通配符的List,我们再看看它的get和add操作:
List<? super Button> buttons = new ArrayList<TextView>();
Object object = button.get(0);// get出来的是Object类型
Button button = ...
buttons.add(button);//add 操作是可以的
解释下,首先?表示未知类型,编译器是不确定它的类型的。
虽然不知道他的具体类型,不过在Java里任何对象都是Object的子类,所以这里能把它赋值给Object。
Button对象一定是这个未知类型的子类,根据多态的特性,这里通过add添加Button对象是合法的。
使用下界通配符? super的泛型List。只能读取到Object对象,一般没有什么实际的使用场景,通常也只拿来添加数据,也就是消费已有的List<? super Button>,往里面添加Button,因此这种泛型类型声明称之为「消费之Consumer」。
小结下,Java的泛型本身是不支持协变和逆变的。
- 可以使用泛型通配符
? extends来使泛型支持协变,但是「只能去读不能修改」,这里的修改仅指对泛型集合添加元素,如果是remove(int index)以及clear当然是可以的 - 可以使用泛型通配符
? super来使泛型支持逆变,但是「只能修改不能读取」,这里说的不能读取是指不能按照泛型类型读取,你如果按照Object读出来再强转当然也是可以的。
根据前面的说法,这被称为PECS法则:「Producer-Extends, Consumer-Super」。
理解了Java的泛型之后,再理解Kotlin中的泛型,容易多了。
说回Kotlin中的out和in
和Java泛型一样,Kotlin中的泛型本身也是不可变的。
- 使用关键字
out来支持协变,等同于Java中的上界通配符? extends - 使用关键字
in来支持逆变,等同于Java中的下界通配符? super
var textView: List<out TextView>
var textView: List<in TextView>
换了个写法,但作用是完全一样的。out表示,我这个变量或者参数只用来输出,不用来输入,你只能读我不能写我;in就反过来,表示它只用来输入,不用来输出,你只能写我不能读我。
你看,我们Android工程师学不会out和in,其实并不是因为这两个关键字多难,而是因为我们应该先学学Java的泛型。
说了这么多List,其实泛型在非集合类的使用也非常广泛,就以「生产者-消费者」为例子:
class Produver<T> {
fun produce(): T {
...
}
}
val producer: Producer<out Text> = Producer<Button>()
val textView: TextView = produver.produce()// 相当于get
再来看看消费者:
class Consumer<T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context))// 相当于add
声明处的out和in
在前面的例子中,在声明Producer的时候已经确定了泛型T只会作为输出来用,但是每次都需要在使用的时候加上out TextView来支持协变,写起来很麻烦。
Kotin提供了另外一种方法:可以在声明类的时候,给泛型符号加上out关键字,表明泛型参数T只会用来输出,在使用的时候就不用额外的out了。
class Producer<out T> {
fun produce(): T {
...
}
}
val producer: Producer<TextView> = Producer<Button>()//这里不写out 也不会报错
val producer: Producer<out TextView> = Producer<Button>()// out可以但是没必要
与out一样,可以在声明类的时候,给泛型参数加上in关键字,来表明这个泛型参数T只用来输入。
class Consummer<in T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<Button> = Consumer<TextView>()// 这里不写in也不会报错
val consumer: Consumer<in Button> = Consumer<TextView>()//in可以但没必要
* 号
前面讲到了Java中单个?号也能作为泛型通配符使用,相当于? extends Object。
它在Kotlin中有等效的写法:*号,相当于out Any。
vat list: List<*>
和Java不同的地方是,如果你的类型定义里已经有了out或者in,那这个限制在变量声明时也依然在,不会被*号去掉。
比如你的类型定义里是out T: Number的,那它加上<*>之后的效果就不是out Any,而是out Number。
where关键字
Java中声明类或接口的时候,可以使用extends来设置边界,将泛型类型参数限制为某个类型的子集:
// T的类型必须是Animal的子类型
class Monster<T extends Animal> {
}
注意这个和前面讲的声明变量时的泛型类型声明时不同的东西,这里并没有?。
同时这个边界是可以设置多个,用&符号连接:
// T 的类型必须同时是Animal和Food的子类型
class Monster<T extends Animal & Food> {
}
Kotlin只是把extends换成了:冒号。
class Monster<T: Animal>
设置多个边界可以使用where关键字:
class Monster<T> where T: Animal, T: Food
有人在看文档的时候觉得这个where是个新东西,其实虽然Java里没有where,但它并没有带来新功能,只能把一个老公能换了个新写法。
Kotlin里where这样的写法可读性更符合英文里的语法,尤其是如果Monster本身还有继承的时候
class Monster<T>: MonsterParent<T> where T: Animal
reified关键字
由于Java中的泛型存在类型擦除的情况,任何在运行时需要知道泛型确切类型信息的操作都没法用了。
比如你不能检查一个对象是否为泛型类型T的实例:
<T> void printIfTypeMatch(Object item) {
if (item instanceof T) {// IDE 会提示错误,illegal generic type for instanceof
System.out.println(item);
}
}
Kotlin里同样也不行:
fun <T> printIfTypeMatch(item: Any) {
if (item is T) {// IDE 会提示错误, Cannot check for instance of earsed type: T
println(item)
}
}
这个问题,在Java中的解决方案通常是额外传送一个Class<T>类型的参数,然后通过Class#isInstance方法来检查:
<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
System.out.println(item);
}
}
Kotlin中同样可以这么解决,不过还有一个更方便的做法:使用关键字reified配合inline来解决:
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) {//这里就不会再提示错误了
println(item)
}
}
Kotlin泛型与Java泛型不一致的地方
- Java里的数组是支持协变的,而Kotlin中的数组
Array不支持协变,因为在Kotlin中数组是用Array类来表示的,这个Array类使用泛型几个集合一样,所以不支持协变。 - Java中的
List接口不支持协变,而Kotlin中的List接口支持协变。Java中的List不支持协变,需要使用泛型通配符来解决。 在Kotlin中,实际上MutableList接口才相当于List。Kotlin中的List接口实现了只读操作,没有写操作,所以不会有类型安全事实上的问题,自然可以支持协变。
练习题
- 实现一个
fill函数,传入一个Array和一个对象,将对象填充到Array中,要求Array参数的泛型支持逆变(假设Arraysize 为 1)。
fun <T> fill(array: Array<in T>, obj: T) {
array[0] = obj
}
fun main() {
val obj = Any()
val array = Array<Any?>(1)
fill(array, obj)
println(array[0])
}
上述实例中,fill函数使用泛型的方法,并接受一个Array<in T>类型的参数和一个泛型对象T。这里使用了in关键字来实现逆变,表示可以接受T的超类型,在fill函数内部,将对象obj设置到Array的指定位置上。
需要注意的是,由于Kotlin中的数组是不变型的,因此无法直接创建一个协变或逆变的数组。在示例上,使用Array<Any?>(1)来创建一个可接受任何类型的数组,并将其作为逆变数组类型的参数传递给fill函数。
2.实现一个 copy 函数,传入两个 Array 参数,将一个 Array 中的元素复制到另外个 Array 中,要求 Array 参数的泛型分别支持协变和逆变。(提示:Kotlin 中的 for 循环如果要用索引,需要使用 Array.indices)
fun <T> copy(source: Array<out T>, destination: Array<in T>) {
for (index in source.indices) {
destination[index] = source[index]
}
}
fun main() {
val sourceArray = arrayOf("apple", "banana", "orage")
val destinationArray = arrayOfNulls<Any>(3)
copy(sourceArray, destinationArray)
println(destinationArray.joinToString())// 输出:apple, banana, orange
}
上述示例中,copy函数使用了泛型方法,并接受一个协变的Array<out T>类型参数的source和一个逆变的Array<in T>类型参数destination。在copy函数内部,使用for循环遍历source.indices,并将source中的元素赋值到destination中相应的位置。
需要注意的是,Array.indices返回一个IntRanger对象,表示索引的范围。正在for循环中使用source.indices可以迭代数组的索引,然后使用索引访问和操作数组元素。
在示例中,sourceArray是一个协变的数组,可以接受其子类型的数组作为参数,而destinationArray是一个逆变的数组,可以接受其超类型的数组作为参数。这样就实现了协变和逆变的数组之间进行元素复制的需求。
版权声明
本文首发于:rengwuxian.com/kotlin-basi…
微信公众号:扔物线