1.引言
Kotlin中的泛型使用和java一样,但如果你使用的是kotlin语言开发,你会发现kotlin的泛型会多出两个关键字,分别是in和out。这两个关键字经常让人疑惑,它的字面意思是输入和输出,很难让人联想到java泛型的某个特性。实际上它们在java中是有对应关系的。
2.java中的泛型通配符
为了更好的理解in、out关键字的作用,我们需要对比java的泛型通配符来看。
先定义一个类:
public class Stack<E>{
void push(E e){...}
E pop(){...}
boolean isEmpty(){...}
}
这是一个栈的声明,为Stack增加一个方法pushAll(),意图是把一个集合的所有元素放入栈中:
public void pushAll(List<E> src){
for (E e : src){
push(e);
}
}
这个方法没有问题,也能实现功能,但并非尽如人意。为什么这么说呢?现在有一个类型参数是TextView的栈Stack<TextView>
,尝试进行以下操作:
Stack<TextView> textViewStack = new Stack<>();
List<Button> buttons = new ArrayList<>();
buttons.add(new Button(this));
buttons.add(new Button(this));
textViewStack.pushAll(buttons); //无法通过编译
我们知道,Button是TextView的子类,一个子类型是可以赋值给父类型的,但上面的代码却无法通过编译,你无法将List<Button>
类型传入pushAll()中。这是因为,虽然Button是Textview的子类,但List<Button>
并不是List<TextView>
子类。这是使用泛型的一个限制。
为什么会有这一限制呢?想必大家都清楚,泛型在编译后会发生发生类型擦除,我们的泛型都被替换成了Object,当我们获取泛型的返回值时系统会自动帮我们强转成泛型类型。为了保证能够正确的转化类型,java加了这一限制。这个就是泛型的不可变性(Invariance)
使用上界通配符? extends
可以突破这个限制。也就是把参数List改成List<?extends E>。List<?extends E>表示“E的某个子类型(包括E)的集合”。
public void pushAll(List<? extends E> src){
for (E e : src){
push(e);
}
}
这样上面的代码就可以通过编译了。当然,pushAll()不仅可以接收List<Button>
,也可以接收List<EditText>
(EditText也是TextView的子类)。
? extends E 可以看作一个E或者E的子类型的“未知类型”,这里的子类包括直接和间接子类。在这里的E就表示父类,父类为上,所以 ? extends被称为上界通配符。这个特性叫做协变(covariant)。
使用上界通配符的好处很明显,我们不需要再增加类似pushAll(List<Button>
src)之类的重载方法了,这些工作看起来徒劳无益且并不优雅。
我们再为Stack类增加一个popAll()的方法,目的是把textViewStack中的元素添加到指定的集合中,一般情况下我们会这样写:
public void popAll(List<E> dst) {
while (!isEmpty()) {
dst.add(pop());
}
}
接下来尝试把textViewStack中的元素添加到List中:
List<View> views = new ArrayList<>();
textViewStack.popAll(views); //无法通过编译
这样的写法看起来并没有问题,使用View的集合来接收TextView是安全的。但这样写还是会报错,popAll()需要传入的参数是List<TextView>
而不是List<View>
。和之前的结论一样,List<View>
和List<TextView>
并没有父子关系。
要想解决这个问题也好办,使用下界通配符? super
即可,也就是把参数List改成List<?super E>。List<?super E>表示“E的某个父类型(包括E)的集合”。
? super E
可以看作一个E或者E的父类的“未知类型”,这里的父类包括直接和间接父类。在这里的E表示子类,子类为下,所以 ? super被称为下界通配符,这个特性叫做逆变.(contravariance)
改造popAll()的让代码通过编译:
public void popAll(List<? super E> dst) {
while (!isEmpty()) { 从
dst.add(pop());
}
}
用一张图来说明这两个通配符的关系:
3.koltin泛型的int和out
相信java的例子很好的解释了泛型通配符的作用。开头说到,Koltin泛型的in和out在java中是由对应关系的。
in,out其实就是对应着java中的 ? extends
和? super
那么回到kotlin来.上述的例子换成koltin的实现就是这样:
class Stack<E> {
fun push(e: E) {...}
fun pop(): E {...}
fun isEmpty(){...}
fun pushAll(src: List<out E>) {
for (e in src) {
push(e)
}
}
fun popAll(dst: MutableList<in E>) {
while (!isEmpty()) {
dst.add(pop())
}
}
}
接下来我们就以in和out来讲解,如果不理解,暂且把先替换成java的 ? extends
和? super
。
使用in和out突破了泛型的不可变性的限制(in使得泛型具有协变性,out使得泛型具有是逆变性),但却增加了另一层限制:
- 使用out修饰的泛型只能用作函数的参数(只能输出不能输入)
- 使用in修饰的泛型类型只能用作函数返回值(只能输入不能输出)
还是用Stack来举例,对于out
来说,你无法对Stack使用push()函数。对于in
来说,你无法通过Stack的pop()函数来获得TextView对象。
why?我们通过反证法来证明为什么会有这个限制:
先说说out:
我们对Stack<out Textview>
调用pop()方法拿到TextView对象是没有问题的,因为Stack<out TextView>
里面存放的对象必然是TextView或者是它的子类,使用父类的引用去接收子类的对象是安全的,就像这样:
val buttonStack: Stack<Button> = Stack()
buttonStack.push(Button(this))
val onlyOutStack : Stack<out TextView> = buttonStack
val tv : TextView = onlyOutStack.pop()
上面的代码编译正常,尽管你里面放的是Button,我用TextView来接收没有问题吧?再来看看这种情况:
val buttonStack: Stack<Button> = Stack()
buttonStack.push(Button(this))
val onlyOutStack : Stack<out TextView> = buttonStack
onlyOutStack.push(TextView(this)) //无法通过编译
val tv : Button = buttonStack.pop() //如果上面的代码可以正常编译,那么这里拿到的就是TextView对象,TextView不能强转成Button
这样看就很好理解了,因为你无法确定Stack<out TextView>
它究竟是哪一个类型的,如果它是Button类型的Stack<Button>
,或者是EditText类型的Stack<EditText>
,你能往里面添加TextView吗?显然是不能的。
现在再来理解in就很简单了:
val textViewStack : Stack<TextView> = Stack()
val onlyInStack : Stack<in Button> = textViewStack
onlyInStack.push(Button(this))
textViewStack.push(TextView(this))
val button : Button = onlyInStack.pop() //无法通过编译,如果可以正常编译的话,那么这里拿到的就是TextView对象,TextView不能强转成Button
我们往TextView的栈Stack添加Button是没有问题的,但拿出来的一定就是Button吗?不一定。
通过反证法就可以很轻松的理解为什么使用in和out打破了一层限制,又多了一层限制。in
和out
也被叫做生产和消费。还是比较形象的,输入即生成,输出即消费。
4.通配符对照
有时我们还会看见java中的?
和kotlin中*
这两种通配符,其实它们只是上界通配符是一种特殊的写法,这种通配符的上界是Object(Any?)。
java:List<?>
等价于 List<? extends Object>
kotlin:List<*>
等价于 List<out Any?>
以下是java和kotlin泛型的对照表:
java | kotlin |
? (? extend Object) | * (out Any?) |
? exent T | out T |
? super T | in T |
5.总结
本文看似在讲kotlin的in、out关键字,其实是把java的泛型基础又复习了一边。kotlin的泛型,原理上和java没有区别,只是写法不一样。无论是java还是kotlin,理解这两个通配符的作用的