图解java泛型的协变和逆变

参考文献:https://www.jianshu.com/p/2bf15c5265c5

https://www.jianshu.com/p/da1127c51c90


今天刚开始看kotlin的泛型语法和概念,觉得之前java中学过泛型,可能这个也差不多吧。。。。。嗯,确实差不多,想着跟之前一样用类比java的方式继续理解kotlin泛型,结果看了两篇java的泛型之后。。。。。。发现java泛型之前没怎么学懂

之前在学java泛型时候没有接触到的两个概念:协变和逆变。下面提到的可能大家都知道,只是我已自己的理解将协变和逆变的概念表述出来:


一、协变逆变概念

逆变与协变用来描述类型转换(type transformation)后的继承关系:A、B表示类型,f(·)表示类型转换,A<=B表示A为B的子类,那么则存在:

  • f(·)是协变的:当A<=B   ,f(A)<=f(B)成立
  • f(·)是逆变的:当A<=B   ,f(A)>=f(B)成立
  • f(·)是不变的:当A<=B   ,f(A) 和f(B)不存在继承关系

看的有点懵逼?先别着急,等会儿回过头来再看这个。。这里介绍了协变和逆变的概念,对于java中数组是协变的,如下所示:

    public static void main(String[] args) {
        String[] strings = new String[5];
        Object[] objects = strings;
    }

实例中创建了一个字符串数组对象,但是用Object数组同样可以引用。

实例中String类<=Object类,对应的String[]<=Object[],所以可以得出数组是协变类型。

现在问题来了:

现在将string类型的数组引用赋值给了object类型的数组引用,在操作时候是不是可以赋值除了string意外的类型呢?

    public static void main(String[] args) {
        String[] strings = new String[5];
        Object[] objects = strings;
        try {
            objects[0] = 1;
            System.out.println(strings[0]);
        } catch (Exception e) {
            System.out.println("出错了吧。。。。。");
            e.printStackTrace();
        }
    }

给objects第一个元素设置一个int类型的1,结果如下:

java.lang.ArrayStoreException: java.lang.Integer
出错了吧。。。。。
	at as.a.Str.main(Str.java:12)

看来数组以协变方式允许类型向上转型,但是会有写入安全的问题,如上异常

 

现在我们看下在集合中使用会是怎么样的:

    public static void main(String[] args) {
        String[] strings = new String[5];
        Object[] objects = strings;
        try {
            objects[0] = 1;
            System.out.println(strings[0]);
        } catch (Exception e) {
            System.out.println("出错了吧。。。。。");
            e.printStackTrace();
        }


        List<String> strList = new ArrayList<>();
        List<Object> objList = strList;//编译错误了
    }

在将strList赋值给objList时候,已经出现编译错误了,错误结果如下:

使用泛型时,在编译期间存在泛型擦除过程,取消了运行时检查,所以将泛型的错误检查提前到了编译器。并不是因为是两个不同的类型。

这时候用到了泛型通配符? extends T  和 ? super T了。。首先示例的继承关系如下:

 


class 生物 {
}

class 动物 extends 生物 {
}

class 人 extends 动物 {
}

class 狗 extends 动物 {
}

class 山顶洞人 extends 人 {
}

class 半坡人 extends 人 {

}

示例如下:

public class Str {
    public static void main(String[] args) {

        List<? extends 动物> objList = new ArrayList<人>();
        动物 动物 = objList.get(0);//编译通过
        生物 动物1 = objList.get(0);//编译通过
        人 人 = objList.get(0);//编译错误
        objList.add(new 动物());//编译错误
        objList.add(new 人());//编译错误
        objList.add(new 狗());//编译错误
    }

}

示例中将动物和生物类型引用objList的元素时,编译无错误,但是将人类型引用objList元素时,编译出错了。然后,,,,,,不管什么类型,只要add就全都编译错误了。

我是这样想的,如果说他允许add T及其子类对象,那他是如何知道哪些类型的对象是应该添加的呢?举个简单的?,List<? extends 动物> 存放都是动物的子类,但是无法确定是哪一个子类,这种情况下依然会出现安全问题(如上栗中String数组中+int);而接收引用也同样是这个道理:我存放的是你T的子类,但是无不知道啊,那我接收的引用只要是你的父类就好啦。

这里简单总结一下上限通配符? extends T 的用法,? extends T表示所存储类型都是T及其子类,但是获取元素所使用的引用类型只能是T或者其父类。使用上限通配符实现向上转型,但是会失去存储对象的能力。上限通配符为集合的协变表示

想要存储对象,就需要下限通配符 ?super T 了,用法如下:

    public static void main(String[] args) {

        List<? super 人> humList = new ArrayList<>();
        humList.add(new 半坡人());//编译通过
        humList.add(new 山顶洞人());//编译通过
        humList.add(new 人());//编译通过
        humList.add(new 动物());//编译失败

    }

相信大家一眼就看出来了,添加人及其子类没有错误,一旦再网上就出现编译错误了。

下限通配符 ? super T表示 所存储类型为T及其父类,但是添加的元素类型只能为T及其子类,而获取元素所使用的类型只能是Object,因为Object为所有类的基类。下限通配符为集合的逆变表示。

现在反过头来看一下最开始说的协变和逆变的概念:

  • 当使用上限通配符时,类的等级越高,所包含的范围越大,符合协变的概念。
  • 当使用下限通配符时,类的等级越高,所包含的范围越小,符合逆变的概念。

以下是笔者对以上内容的总结四句话:

?extends T 存放的类型一定为T及其子类,但是获取要用T或者其父类引用。转型一致性

 

?super T 存放的类型一定为T的父类,但添加一定为T和其子类对象。转型一致性

 

?extends T 进行add(T子类)编译出错:因为无法确定到底是哪个子类

 

?super T get()对象,都是Object类型,因为T的最上层父类是Object,想要向下转型只能强转。

 

对于泛型还有生产者消费者的概念,笔者打算放在下一篇和kotlin的泛型卸写在一起。

 

转自:https://blog.csdn.net/zy_jibai/article/details/90082239

posted @ 2022-03-23 15:09  甜菜波波  阅读(970)  评论(0编辑  收藏  举报