1

ps:阅读原文,可以获取源码

在 kotlin 语言中,out 表示协变,in 表示逆变;协变和逆变并不是 kotlin 独有的概念,像 Java、C#都有这样的概念;为了能够理解 kotlin 语言中的 out 和 in,我们先用 Java 的泛型来举例,我们需要用泛型,是因为它的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。

1、Java 中的 ? extends T 和 ? super T

1、1 ? extends T

ps:代码是在 AndroidStudio 工具上写的

建立一个 Java 文件鸟类 Birds;

public class Birds {
 private String name;
 public Birds(String name) {
 this.name = name;
 }
 public void flight() {
 System.out.println("我是" + name + ",属于鸟类,我能飞行");
 }
}

建立一个 Java 文件乌鸦类 Crow 并继承 Birds;

public class Crow extends Birds {
 public Crow(String name) {
 super(name);
 }
}

新建一个 Java 文件的泛型类 TestBirds 并限制泛型 T 是 Birds 的子类;

public class TestBirds<T extends Birds> {
 public void actionBirds(List<T> birds) {
 for (T bird : birds) {
 bird.flight();
 }
 }
}

在程序入口尝试使用 List<Crow> list 作为参数传递给 TestBirds 的 actionBirds 方法;

 List<Crow> list = new ArrayList<>();
 TestBirds<Birds> testBirds = new TestBirds<>();
 Crow crow = new Crow("乌鸦");
 list.add(crow);
 
 /**
 * 这里 list 地方会编译报红线
 */
 testBirds.actionBirds(list);

这时候传入 list 发现编译不通过,我们这里分析一下:TestBirds 是泛型类,没有使用的之前,T 是不确定的,使用之后 T 是确定的,它是 Birds;把 list 作为参数传入 actionBirds 方法中,等同于 List<Birds> birds = list,但是 List<Birds> birds = list 是不成立的,虽然 Crow 继承于 Birds,birds 只保存的是 Birds 类型的对象,list 只保存 Crow 类型的对象,birds 和 list 是没有任何关系的。

TestBirds 类中新添加一个 actionBirds2 方法,该方法是在 actionBirds 方法的基础上修改的;

 public void actionBirds2(List<? extends T> birds) {
 for (T bird : birds) {
 bird.flight();
 }
 }

在程序入口使用 List<Crow> list 作为参数传递给 TestBirds 的 actionBirds2 方法;

 List<Crow> list = new ArrayList<>();
 TestBirds<Birds> testBirds = new TestBirds<>();
 Crow crow = new Crow("乌鸦");
 list.add(crow);
 
 /**
 * 这里 list 地方会编译报红线
 */
//        testBirds.actionBirds(list);
 
 testBirds.actionBirds2(list);

这时候发现 testBirds.actionBirds2(list) 这行代码编译通过了,是不是感觉很神奇?这里分析一下:把 list 作为参数传入 actionBirds2 方法相等于 List<? extends Birds> birds = list,它是成立的,List<? extends Birds> birds ,表示集合存储的是 Birds 和 Birds 的子类对象,限定了上届,而 list 存储的是 Birds 子类的对象,所以代码编译通过,它是成立的;上界通配符 < ? extends T>,用 extends 关键字声明,表示参数可能是T,或者是T的子类。

1、2 ? super T

在上面原有代码的基础上,在 TestBirds 类中添加一个 actionBirds3 方法;

 public void actionBirds3(List<T> birds,List<T> crows) {
 for (T crow : crows) {
 birds.add(crow);
 }
 }

在程序入口尝试调用 TestBirds 的 actionBirds3 方法;

 List<Crow> list = new ArrayList<>();
 TestBirds<Crow> testBirds = new TestBirds<>();
 Crow crow = new Crow("乌鸦");
 list.add(crow);
 List<Birds> birdsList = new ArrayList<>();
 testBirds.actionBirds3(birdsList,list);

到这里后,发现 actionBirds3 方法的第一个参数就报红色编译错误了,原因是实例化 TestBirds 类对象的时候,T 被 Crow 替换了,birdsList 只存储 Birds 类型的数据,而 actionBirds3 方法的第一个参数只存储 Crow 类型的数据,所以2者没有任何关系,所以语法编译错误。

我们在 TestBirds 中新添加一个 actionBirds4 方法,在 actionBirds3 的基础上改动一下第一个参数;

 public void actionBirds4(List<? super T> birds,List<T> crows) {
 for (T crow : crows) {
 birds.add(crow);
 }
 }

在实参不变的情况下,在程序入口调用 TestBirds 的 actionBirds4 方法;

 List<Crow> list = new ArrayList<>();
 TestBirds<Crow> testBirds = new TestBirds<>();
 Crow crow = new Crow("乌鸦");
 list.add(crow);
 List<Birds> birdsList = new ArrayList<>();
 
 /**
 * 这里 birds 地方会编译报红线
 */
//        testBirds.actionBirds3(birds,list);
 testBirds.actionBirds4(birdsList,list);

这时候发现调用 TestBirds 中的 actionBirds4 方法编译通过,分析一下:actionBirds4 方法的第一个参数为 List<? super T> birds,? super T 是下限通配符,它表示在使用中限定参数类型为 T 或者是 T 的父类,birds 存储的是 T 类型和 T 父类的对象;在实例化 TestBirds 的过程中,把 Crow 替换成了 T,Birds 刚好是 Crow 的父类,调用 TestBirds 的 actionBirds4 方法传递第一个参数时相当于 List<? super Crow> birds = list,所以编译通过。

2、kotlin 中的 out 和 in

2、1 out

在上面 ? extends T 的代码案例中,TestBirds 类中的 actionBirds2 方法的第一个参数 birds 集合加了 ? extends T 进行限制,然后用 for 循环遍历 T 的元素进行取出来,这样的操作是读取;? extends T 限定了通配符类型的上界,所以我们可以安全地从其中读取却不可以修改数据;我们可以把那些只能从中读取的对象称为生产者;List<? extends T> 这样的类型不进行消费的生产者,以保证类型运行的安全,这就是协变;在 kotlin 中用 out 表示,kotlin 中的 “out T” 等同于 Java 的 “?extends T”;下面用 kotlin 的 out 关键字举个例子:

新建一个 kotlin 类 TestBirds2 并写一个和 TestBirds 类 actionBirds2 效果一样的函数;

class TestBirds2<T: Birds> {
 fun actionBirds2(birds: MutableList<out T>) {
 for (bird: T in birds) {
 bird.flight()
 }
 }
}

在程序入口调用 TestBirds2 中的 actionBirds2 函数;

 var testBirds2: TestBirds2<Birds> = TestBirds2<Birds>()
 var crow: Crow = Crow("乌鸦")
 var crowList: MutableList<Crow> = mutableListOf(crow)
 testBirds2.actionBirds2(crowList)

2、2 in

在上面 ? super T 的代码案例中,TestBirds 类中的 actionBirds4 方法的第一个参数 birds 集合加了 ? super T 进行限制,然后用 for 循环遍历第二个参数crows 的 T 元素进行取出来,再将 T 元素放入到 birds 集合中;? super T 限定了通配符类型的下界,所以我们可以安全地从其中修改数据,也就是将 T 元素放入到 birds 集合中;我们可以把那些只能从中修改的对象称为消费者;List<? super T> 这样的类型获取出来的数据类型是 Object,没有意义,可认为不进行生产的消费者,以保证类型运行的安全,这就是逆变;在 kotlin 中用 in 表示,kotlin 中的 “in T” 等同于 Java 的 “?super T”;下面用 kotlin 的 in 关键字举个例子:

在 TestBirds2 类中写一个和 TestBirds 类中 actionBirds4 方法效果一样的函数;

 fun actionBirds4(birds: MutableList<in T>,crow: MutableList<T>) {
 for (t: T in crow) {
 birds.add(t) 
 }
 }

在程序入口调用 TestBirds2 中的 actionBirds4函数;

 var testBirds2: TestBirds2<Crow> = TestBirds2<Crow>()
 var crow: Crow = Crow("乌鸦")
 var crowList: MutableList<Crow> = mutableListOf(crow)
 var birdsList: MutableList<Birds> = mutableListOf()
 testBirds2.actionBirds4(birdsList,crowList)

2、3 类型投影

上面的 out 和 in 的例子使用起来还是有限制,因为有 T 继承于 Birds 的局限;这里讲一下类型投影,在讲类型投影之前先说一下 Any,Any 是 kotlin 语言的祖宗类,类似于 Java 中的 Object,但是又不是等于 Object,因为 Any 只有 equals、hashCode 和 toString 这3个函数;将一个类声明为泛型类,泛型类型可以出现在 out 位置,也可以出现在 in 位置,我们就可以在使用处将其声明成协变或者逆变,就等于把这个类型投影出某一面进行使用,就属于类型投影;就拿泛型类 MutableList<T> 来说,真正要实例化 MutableList 的时候,T 的位置可以多添加 in 或者 out, 把这个类型投影出某一面进行使用,也就是 MutableList 的读取数据方法 get 或者写入数据方法 add;下面就拿 MutableList 写代码举例一下:

 var mutableList: MutableList<out Any> = mutableListOf("公众号小二玩编程",2,3,4,5)
 var size: Int = mutableList.size - 1
 var any: Any? = null
 for (i: Int in 0 .. size) {
 any = mutableList.get(i)
 println("第" + (i + 1) + "any是--" + any)
 }
 var mutableList2: MutableList<in String> = mutableListOf()
 mutableList2.add("公众号小二玩编程")
 
 /**
 * 这里 Int 类型,编译会报错,因为 mutableList2 做了 in String 限制
 */
 mutableList2.add(2)

本篇文章写到这里就结束了,由于技术水平有限,文章中难免会有错误,欢迎大家批评指正。


公众号小二玩编程
4 声望4 粉丝

活到老,学到老。