当前位置: 首页 > news >正文

JAVA中的协变与逆变

JAVA中的协变与逆变
首先说一下关于Java中协变,逆变与不变的概念

比较官方的说法是逆变与协变描述的是类型转换后的继承关系。

定义A,B两个类型,A是由B派生出来的子类(A<=B),f()表示类型转换如new List();

协变: 当A<=B时,f(A)<=f(B)成立
逆变: 当A<=B时,f(B)<=f(A)成立
不变: 当A<=B时,上面两个式子都不成立
这么说可能理解上有些费劲,我们用代码来表示一下协变和逆变

class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}

@Test
public void testArray() {
    Fruit[] fruit = new Apple[10];
    fruit[0] = new Apple();
    fruit[1] = new Jonathan();
    try {
        fruit[0] = new Fruit();
    } catch (Exception e) {
        System.out.println(e);
    }
    try {
        fruit[0] = new Orange();
    } catch (Exception e) {
        System.out.println(e);
    }
}

Java中数组是协变的,可以向子类型的数组赋基类型的数组引用。

Apple是Fruit的子类型,所以Apple的对象可以赋给Fruit对象。Apple<=Fruit Fruit的数组类型是Fruit[],这个就是由Fruit对象构造出来的新的类型,即f(Fruit),同理,Apple[]就是Apple构造出来的新的类型,就是f(Apple)

所以上方代码中的Fruit[] fruit = new Apple[10]是成立的,这也是面向对象编程中经常说的

子类变量能赋给父类变量,父类变量不能赋值给子类变量。
上方代码中的try..catch中的在编译器中是不会报错的,但是在运行的时候会报错,因为在编译器中数组的符号是Fruit类型,所以可以存放Fruit和Orange类型,但是在运行的时候会发现实际类型是Apple[]类型,所以会报错

java.lang.ArrayStoreException: contravariant.TestContravariant$Fruit
java.lang.ArrayStoreException: contravariant.TestContravariant$Orange
不变

@Test
public void testList() {
    List<Fruit> fruitList = new ArrayList<Apple>();
}

这样的代码在编译器上会直接报错。和数组不同,泛型没有内建的协变类型,使用泛型的时候,类型信息在编译期会被类型擦除,所以泛型将这种错误检测移到了编译器。所以泛型是 不变的

泛型的协变

但是这样就会出现一些很别扭的情况,打个比方就是一个可以放水果的盘子里面不能放苹果。

所以为了解决这种问题,Java在泛型中引入了通配符,使得泛型具有协变和逆变的性质, 协变泛型的用法就是<? extends Fruit>

@Test
public void testList() {
    List<? extends Fruit> fruitList = new ArrayList<Apple>();
    // 编译错误
    fruitList.add(new Apple());
    // 编译错误
    fruitList.add(new Jonathan());
    // 编译错误
    fruitList.add(new Fruit());
    // 编译错误
    fruitList.add(new Object());
}

当使用了泛型的通配符之后,确实可以实现将ArrayList进行向上转型了,实现了泛型的协变,但是却再也不能往容器中放任何东西了,连Apple本身都被禁止了

因为,在定义了fruitList之后,编译器只知道容器中的类型是Fruit或者它的子类,但是具体什么类型却不知道,编译器不知道能不能比配上就都不允许比配了。类比数组,在编译器的时候数组允许向数组中放Fruit和Orange等非法类型,但是运行时还是会报错,泛型是将这种检查移到了编译期,协变的过程中丢失了类型信息。

所以对于通配符,T和?的区别在于,T是一个具体的类型,但是?编译器并不知道是什么类型。不过这种用法并不影响从容器中取值。

List<? extends Fruit> fruitList = new ArrayList();

Fruit fruit = fruitList.get(0);

Object object = fruitList.get(0);
// 编译错误
Apple apple = fruitList.get(0);
泛型的逆变

@Test
public void testList() {
List<? super Apple> appleList = new ArrayList();
// 编译错误
Fruit fruit = appleList.get(0);
// 编译错误
Apple apple = appleList.get(0);
// 编译错误
Jonathan jonathan = appleList.get(0);

    Object object = appleList.get(0);

    appleList.add(new Apple());

    appleList.add(new Jonathan());
    // 编译错误
    appleList.add(new Fruit());
    // 编译错误
    appleList.add(new Object());
}

可以看到使用super就可以实现泛型的逆变,使用super的时候指出了泛型的下界是Apple,可以接受Apple的父类型,既然是Apple的父类型,编辑器就知道了向其中添加Apple或者Apple的子类是安全的了,所以,此时可以向容器中进行存,但是取的时候编辑器只知道是Apple的父类型,具体什么类型还是不知道,所以只有取值会出现编译错误,除非是取Object类型。

泛型协变逆变的用法

当平时定义变量的时候肯定不能像上面的例子一样使用泛型的通配符,具体的泛型通配符的使用方法在Effective Jave一书的第28条中有总结:

为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果每个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型比配,这是不用任何通配符而得到的。

简单来说就是PECS表示->producer-extends,consumer-super。

不要使用通配符类型作为返回类型,除了为用户提供额外的灵活性之外,它还会强制用户在客户端代码中使用通配符类型。通配符类型对于类的用户来说应该是无形的,它们使方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数,如果类的用户必须考虑通配符类型,类的API或许就会出错。

一个经典的例子就是java.uitl.Collections中的copy方法

public static void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");

    if (srcSize < COPY_THRESHOLD ||
        (src instanceof RandomAccess && dest instanceof RandomAccess)) {
        for (int i=0; i<srcSize; i++)
            dest.set(i, src.get(i));
    } else {
        ListIterator<? super T> di=dest.listIterator();
        ListIterator<? extends T> si=src.listIterator();
        for (int i=0; i<srcSize; i++) {
            di.next();
            di.set(si.next());
        }
    }
}

dest为生产者只从其中取数据,src为消费者,只存放数据进去。

转载于:https://www.cnblogs.com/jinlin/p/9397695.html

相关文章:

  • bzoj4004: [JLOI2015]装备购买
  • 【iOS-Cocos2d游戏开发】关于CCSpriteSheet报错问题
  • 《C#多线程编程实战》2.7 CountDownEvent
  • linux下安装Bugzilla(二)
  • 2018-8-1 列表的增删改查及元组的基本操作
  • linux下安装Bugzilla(三)
  • mysql sql优化的一些总结
  • linux下安装Bugzilla(四)
  • Centos7 下安装配置tomcat7
  • 【iOS-Cocos2d游戏开发】使用plist文件制作简单精灵
  • linux awk详解
  • HDU 2680 Choose the best route(多起点单终点最短路问题)题解
  • 【iOS-Cocos2d游戏开发】使用Zwoptex生成plist文件
  • 初始Windows系统
  • 西方酒馆(一)
  • 【划重点】MySQL技术内幕:InnoDB存储引擎
  • Docker入门(二) - Dockerfile
  • express + mock 让前后台并行开发
  • JavaScript 是如何工作的:WebRTC 和对等网络的机制!
  • js 实现textarea输入字数提示
  • mockjs让前端开发独立于后端
  • Object.assign方法不能实现深复制
  • RxJS: 简单入门
  • 阿里研究院入选中国企业智库系统影响力榜
  • 给Prometheus造假数据的方法
  • 关于for循环的简单归纳
  • 你不可错过的前端面试题(一)
  • 算法系列——算法入门之递归分而治之思想的实现
  • 小李飞刀:SQL题目刷起来!
  • UI设计初学者应该如何入门?
  • 机器人开始自主学习,是人类福祉,还是定时炸弹? ...
  • #HarmonyOS:基础语法
  • (173)FPGA约束:单周期时序分析或默认时序分析
  • (附源码)springboot车辆管理系统 毕业设计 031034
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (机器学习的矩阵)(向量、矩阵与多元线性回归)
  • (四) 虚拟摄像头vivi体验
  • (小白学Java)Java简介和基本配置
  • (一)kafka实战——kafka源码编译启动
  • (一)spring cloud微服务分布式云架构 - Spring Cloud简介
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转载)微软数据挖掘算法:Microsoft 时序算法(5)
  • .java 9 找不到符号_java找不到符号
  • .jks文件(JAVA KeyStore)
  • .NET Conf 2023 回顾 – 庆祝社区、创新和 .NET 8 的发布
  • .NET Core 2.1路线图
  • .NET 发展历程
  • .Net(C#)自定义WinForm控件之小结篇
  • .NET:自动将请求参数绑定到ASPX、ASHX和MVC(菜鸟必看)
  • .net6Api后台+uniapp导出Excel
  • .NET导入Excel数据
  • .Net的C#语言取月份数值对应的MonthName值
  • .net用HTML开发怎么调试,如何使用ASP.NET MVC在调试中查看控制器生成的html?
  • .net知识和学习方法系列(二十一)CLR-枚举
  • .w文件怎么转成html文件,使用pandoc进行Word与Markdown文件转化