重学Kotlin之泛型的逆变和协变

简介: 泛型的逆变和协变

前言

上一篇文章重学Kotlin之那些你没注意到的细节中在写泛型那一块的时候只写了泛型的实化,本来想直接把泛型的逆变和协变写了的,但由于篇幅原因就没写,所以有了这篇文章。

故事开始

周五的下午,小老弟儿把手里的活都干完了,闲来无事在网上溜达 Kotlin 相关的知识,还带着华子😂。看着看着看到了我上回写的那篇文章。。。

“好大哥,你写的那篇文章中的泛型那块讲到逆变和协变的时候就没写了,你给我讲讲呗,来,好大哥,来根华子!抽这个不咳嗽。”

“华子自己留着抽吧,我咳嗽不来了。逆变和协变是吗?正好我也准备写文章了,那就先给你讲一遍理理思路吧。”

“感谢好大哥!我在网上溜达的时候发现 Kotlin 中逆变和协变又有两个关键字:in 和 out ,这是啥意思啊?”

“怎么说呢,Kotlin 泛型中的逆变和协变感觉和 Java 泛型中的逆变协变一样啊!”

“啊?Java 中也有逆变协变嘛?。。。。”

Java中的逆变协变

“先带你看看Java 中的逆变协变吧,Java 会了 Kotlin 就很简单了。”

来吧,先来点简单的,咱们慢慢来!

public class Person {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }  
    public void toWork() {
        System.out.println("我是工人"+getName()+",我要好好干活!!!");
    }
}

新建一个Person类,写了两个属性,还有一个待实现的 toWork() 方法。

public class Worker1 extends Person {
    public Worker1(String name, int age) {
        super(name, age);
    }
    @Override
    public void toWork() {
        System.out.println("我是1工人"+getName()+",我要好好干活!!!");
    } 
}

Worker1 继承自 Person 类,并重写了 toWork() 方法。

public class Worker2 extends Person {
    public Worker2(String name, int age) {
        super(name, age);
    }
    @Override
    public void toWork() {
        System.out.println("我是2工人"+getName()+",我也要好好干活!!!");
    }
}

Worker2 没什么好说的,和 Worker1 一样,只是类名有点区别。

协变

“现在说的都没问题吧?”

“好大哥,看你说的,这我还能不会嘛!您快继续吧。”

“好,现在问你一个问题,你看下面的代码会报错吗?”

List<Person> personArrayList = new ArrayList<>();
personArrayList.add(new Person("aaa",11));
personArrayList.add(new Worker1("bbb",12));
personArrayList.add(new Worker2("ccc",13));

“肯定不会啊,因为 Worker1 和 Worker2 都是 Person 的子类,所以这么写是可以的!”

“小老弟儿不错哈,那再看看下面的代码会报错嘛?”

public static void main(String[] args) {
        List<Person> personArrayList = new ArrayList<>();
        personArrayList.add(new Person("aaa",11));
        personArrayList.add(new Worker1("bbb",12));
        personArrayList.add(new Worker2("ccc",13));
        List<Worker1> personArrayList1 = new ArrayList<>();
        personArrayList1.add(new Worker1("ddd",14));
        List<Worker2> personArrayList2 = new ArrayList<>();
        personArrayList2.add(new Worker2("eee",15));
        setWork(personArrayList);
        setWork(personArrayList1);
        setWork(personArrayList2);
    }
    public static void setWork(List<Person> studentList) {
     for (Person o : studentList) {
            if (o != null){
                o.toWork();
            }
        }
    }

“等会啊,代码有点多,我捋一捋。首先建一个 Person 的集合,然后放入刚才的数据,然后建一个 Worker1 和 Worker2 的集合,再调用 setWork() 方法,应该是没问题的吧?”

“嘿嘿,小老弟儿,这就有问题了,你看!”

“啊?这是为啥啊,Worker1 和 Worker2 不是 Person 的子类嘛?”

“Worker1 和 Worker2 是 Person 的子类,但是 List 和 List 并不是 List 的子类啊!”

“好大哥好大哥,快说说怎么解决吧。”

“这就要进入今天的主题了----协变!上面说过了 List 和 List 并不是 List 的子类,那么咱们需要做的就是让 List 和 List 变成 List 的子类,做法很简单,只要加上 ? extends 就可以了”

public static void setWork(List<? extends Person> studentList) {
        for (Person o : studentList) {
            if (o != null){
                o.toWork();
            }
        }
    }

“这么简单吗?”

“对,就这么简单,咱们运行下看看吧”

我是工人aaa,我要好好干活!!!
我是1工人bbb,我要好好干活!!!
我是2工人ccc,我也要好好干活!!!
我是1工人ddd,我要好好干活!!!
我是2工人eee,我也要好好干活!!!

“哇!真的可以啊!那以后写泛型我就全部都写成协变的不得了!这样所有的子类就都可以这样用了!”

“千万别!这样做肯定是有限制的!协变的泛型只能获取但不能修改了,比如在 List 中就只能 get 而不能 add 了 ,用的时候一定要注意!”

“啊?我不信!”

“哎,你非不信的话咱们就来试试不得了!

“啊!真的是啊!不对,你往里添加的是 Worker1 ,但是泛型是 Person!”

“刚刚不是说了嘛!这已经是协变了,是可以的。”

“好大哥,你改成 Peoson 再试试!”

“好好好,改,你看!”

“啊?为啥啊!好大哥,快说吧,别卖关子了!”

“哈哈哈,行。你想一下啊,咱们刚才把泛型改成了协变的,意思就是上界咱们定为了 Person,但是咱们的类型写的是 ? ,编译器并不知道咱们给它的具体类型是什么,只要是继承自 Person 的类就可以,所以 get 出的对象肯定是 Person 的子类型,根据多态的特性,所以能够直接赋值给 Person ,但是 add 就不可以了,咱们可能添加的是 List 或 List ,还有可能是 List,所以编译器无法确定咱们添加的到底是什么类型就无法继续执行了,肯定就报错了! ”

“奥,这样说我好像有点理解了。”

逆变

“其实协变搞明白之后逆变就简单了,和协变是正好相反的!咱们再来看一个方法吧!你看看这个会报错嘛!”

List<Person> personArrayList = new ArrayList<>();
      personArrayList.add(new Person("aaa",11));
        personArrayList.add(new Worker1("bbb",12));
        personArrayList.add(new Worker2("ccc",13));
        List<Worker1> personArrayList1 = new ArrayList<>();
        personArrayList1.add(new Worker1("ddd",14));
        setWorker(personArrayList);
        setWorker(personArrayList1);
    public static void setWorker(List<Worker1> studentList) {
        for (Object o : studentList) {
            System.out.println("哈哈 "+o.toString());
        }
    }

“这个。。。应该会报错,方法接收的是 List ,但是传的却有 List ,而且 Woker1 是 Person 的子类,而不是 Person 是 Worker1 的子类”

“不错,小老弟儿变聪明了,哈哈哈。确实是不可以的,原因和你说的差不多,但是这种情况下咱们就可以使用逆变了”

public static void setWorker(List<? super Worker1> studentList) {
    for (Object o : studentList) {
        System.out.println("哈哈 "+o.toString());
    }
}

“好大哥,逆变是不是和协变一样也有限制?”

“没错,逆变和协变一样,类型也是 ?,不过 ? extends 是上界通配符,而 ?super 是下界通配符,它的范围包括 Worker1 和它的父类。和协变相反,逆变中是可以 add 的,因为 Worker1 一定是这个未知类型的子类型,所以是可以 add 的。这里也没有 get 的限制,会变成 Object ,因为在 Java 中所有类型都是 Object 的子类。“

“奥,逆变也不难嘛!”

Kotlin中的逆变协变

“小老弟,其实如果懂 Java 中的逆变和协变的话那么 Kotlin 的逆变和协变基本不用学了。。因为基本完全一样!你说不懂 Kotlin 的逆变和协变其实是不懂 Java 的逆变和协变。来看下 Kotlin 的逆变和协变吧!还是先来写几个类,和上面的一样,不过是使用 Kotlin 编写”

open class Person(val name: String, val age: Int)  {
    open fun toWork() {
        println("我是工人$name,我要好好干活!!!")
    }
}
class Worker1(name: String, age: Int) : Person(name, age) {
    override fun toWork() {
        println("我是1工人$name,我要好好干活!!!")
    }
}
class Worker2(name: String, age: Int) : Person(name, age) {
    override fun toWork() {
        println("我是2工人$name,我也要好好干活!!!")
    }
}

Kotlin的协变 - out

“好大哥,说了半天了,in 和 out关键字到底怎么用你还没说呢!”

“别着急,你看看代码就懂了!”

fun main() {
    val personArrayList: MutableList<Person> = ArrayList()
    personArrayList.add(Person("aaa", 11))
    personArrayList.add(Worker1("bbb", 12))
    personArrayList.add(Worker2("ccc", 13))
    val personArrayList1: MutableList<Worker1> = ArrayList()
    personArrayList1.add(Worker1("ddd", 14))
    val personArrayList2: MutableList<Worker2> = ArrayList()
    personArrayList2.add(Worker2("eee", 15))
    setWork(personArrayList)
    setWork(personArrayList1)
    setWork(personArrayList2)
}
fun setWork(studentList: List<out Person>) {
    for (o in studentList) {
        o.toWork()
    }
}

“啊,大哥,我好像明白了,这。。。。和 Java 的协变一样啊,只是关键字不同!”

“哈哈哈,所以说只要懂了 Java 其实 Kotlin 并不难!你再仔细看下 setWork() 这个方法有什么问题。”

fun setWork ( studentList : List < out Person >){
 for ( o in studentList ){
 o . tolWork ()
}
}

“这个方法提醒咱们这里的 out 关键字可以省略掉!不对啊,那省略了就会报错啊!”

“哈哈哈,这其实就是 Kotlin 为咱们做的事,Kotlin 中 List 是只读的,所以说肯定是安全的,所以官方在定义 List 接口的时候就直接定义成了协变的!”

/**
* A generic ordered collection of elements . Methods in this inter 
* read / write access is supported through the [ MutableList ] interf *@ param E the type of elements contained in the List . The List i : public interface Listcout E >
// Query 0perations
 E >: Collection < E >{
 override val size : Int 
 override fun isEmpty (): Boolean 
 override fun contains ( element :@ UnsafeVariance E ): Boolean
 override fun iterator (): Iterator < E >

Kotlin的逆变 - in

“好大哥,逆变我觉得我应该会了,我帮你改写下刚才 Java 逆变的方法吧!”

fun setWorker(studentList: MutableList<in Worker2>) {
    for (o in studentList) {
        println("哈哈 " + o.toString())
    }
}

“没错,就是这么简单!”

总结

“好大哥,你说为什么会有逆变和协变呢?为什么不可以直接都能使用,非要搞这么复杂呢?”

“你知道泛型的类型擦除吗?”

“额。。。听说过。”

“因为 Java 的泛型类型在编译的时候会发生类型擦除,为了保证类型安全,所以才。。。。算了,之后再说吧,这个说起来有点多,下回好好给你讲!准备下班了小老弟,明天放假,准备干啥去啊?”

“当然是出去玩啊!”

“疫情期间,尽量少出门吧。下班了,走了。”



目录
相关文章
|
6月前
|
安全 Java Kotlin
Kotlin泛型:灵活的类型参数化
Kotlin泛型:灵活的类型参数化
|
4月前
|
缓存 安全 Android开发
Android经典实战之用Kotlin泛型实现键值对缓存
本文介绍了Kotlin中泛型的基础知识与实际应用。泛型能提升代码的重用性、类型安全及可读性。文中详细解释了泛型的基本语法、泛型函数、泛型约束以及协变和逆变的概念,并通过一个数据缓存系统的实例展示了泛型的强大功能。
44 2
|
6月前
|
安全 Kotlin 容器
Kotlin 中的协变与逆变
Kotlin 中的协变与逆变
238 1
|
2月前
|
存储 安全 Java
Kotlin教程笔记(30) - 泛型详解
Kotlin教程笔记(30) - 泛型详解
31 3
|
2月前
|
存储 安全 Java
Kotlin教程笔记(30) - 泛型详解
Kotlin教程笔记(30) - 泛型详解
|
2月前
|
存储 安全 Java
Kotlin教程笔记(30) - 泛型详解
Kotlin教程笔记(30) - 泛型详解
24 0
|
2月前
|
存储 安全 Java
Kotlin教程笔记(30) - 泛型详解
本教程详细讲解了Kotlin中的泛型概念,包括协变、逆变、类型投影及泛型函数等内容。适合已有Java泛型基础的学习者,深入理解Kotlin泛型机制。快速学习者可参考“简洁”系列教程。
29 0
|
6月前
|
安全 Java 编译器
Android面试题之Java 泛型和Kotlin泛型
**Java泛型是JDK5引入的特性,用于编译时类型检查和安全。泛型擦除会在运行时移除类型参数,用Object或边界类型替换。这导致几个限制:不能直接创建泛型实例,不能使用instanceof,泛型数组与协变冲突,以及在静态上下文中的限制。通配符如<?>用于增强灵活性,<? extends T>只读,<? super T>只写。面试题涉及泛型原理和擦除机制。
41 3
Android面试题之Java 泛型和Kotlin泛型
|
Kotlin
Kotlin中接口、抽象类、泛型、out(协变)、in(逆变)、reified关键字的详解
Kotlin中接口、抽象类、泛型、out(协变)、in(逆变)、reified关键字的详解
101 0
|
安全 Java 编译器
Kotlin 泛型 VS Java 泛型
Kotlin 泛型 VS Java 泛型
86 0