如何理解Kotlin泛型中的in和out?

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打破了一层限制,又多了一层限制。inout也被叫做生产和消费。还是比较形象的,输入即生成,输出即消费。

4.通配符对照

有时我们还会看见java中的和kotlin中*这两种通配符,其实它们只是上界通配符是一种特殊的写法,这种通配符的上界是Object(Any?)。

java:List<?> 等价于 List<? extends Object>
kotlin:List<*> 等价于 List<out Any?>

以下是java和kotlin泛型的对照表:

javakotlin
? (? extend Object)* (out Any?)
? exent Tout T
? super Tin T

5.总结

本文看似在讲kotlin的in、out关键字,其实是把java的泛型基础又复习了一边。kotlin的泛型,原理上和java没有区别,只是写法不一样。无论是java还是kotlin,理解这两个通配符的作用的可以让泛型的使用更具灵活性。
更多安卓知识体系,请关注公众号:
三分钟Code

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值