• 图解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

  • 相关阅读:
    Java反射实现几种方式
    kettle之几个.bat初步介绍
    eclipse 启动项目时候报的错误
    转:kettle中调度kitchen.bat后面接参数,写定时更新任务
    hibernate4,manytomany
    hibernate4,onetomany
    Javascript系列之在HTML中使用JavaScript
    Javascript系列之js简介
    004 C++ 指针
    003 C++ 函数
  • 原文地址:https://www.cnblogs.com/tiancai/p/16044525.html
Copyright © 2020-2023  润新知