java中的协变与逆变

司霖      2022-04-06     597

关键词:

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

因为,在定义了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

    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

    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为消费者,只存放数据进去。













厘清泛型参数的协变与逆变

协变与逆变(CoVariantandContraVariant),很多人是糊涂的,我也一直糊涂。其实,对协变与逆变概念糊涂,甚至完全不知道,对一般程序员也没有很大影响。不过,如果你想提高水平,想大概看懂.NetFramework类库中那些泛型接口与泛... 查看详情

c#中的协变与逆变(代码片段)

一:什么是协变与逆变官方解释:协变指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,逆变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 更易理解... 查看详情

java逆变与协变(待完善)

...变逆变:若B是A的子类,且F(B)是F(A)的父类,则F为逆变java中的协变:B是A的子类,则List是List的子类java中的逆变:B是A的子类,则List是List的子类java中协变与逆变的约束:java中的协变逆变和约束,都是出于对多态的应用。多态:... 查看详情

java泛型中的协变和逆变

Java泛型中的协变和逆变一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的。但当我们在Java泛型中引入通配符这个概念的时候,Java其实是支持协变和逆变的。看下面几... 查看详情

markdown[java中的协变和逆变]#java(代码片段)

查看详情

协变与逆变(代码片段)

...2/14/%E5%8D%8F%E5%8F%98%E4%B8%8E%E9%80%86%E5%8F%98/记录 官方文档的协变与逆变学习过程。使用举例协变与逆变能够实现数组类型、委托类型和泛型接口参数的隐式引用转换。1、委托类型namespaceConsoleApp4classProgramstaticvoidMain(string[]args)Func&... 查看详情

typescript中的协变和逆变

参考技术ATypescript的协变和逆变和C#Scala中的类似,但是Typescript的会自动算出来接口属于协变还是逆变,C#Scala中需要显示声明inout标记接口。在typescript需要在tsconfig中使用strictFunctionTypes参数开启逆变检查,否则就是双变(协变或者... 查看详情

java进阶知识点2:看不懂的代码-协变与逆变

要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了。我们知道,在Java的世界中,存在继承机制。比如MochaCoffee类是Coffee类的派生类,那么我们可以在任何时候使用MochaCoffee类的引用去替换... 查看详情

编程语言中的协变和逆变有啥区别? [关闭]

】编程语言中的协变和逆变有啥区别?[关闭]【英文标题】:Whatisthedifferencebetweencovarianceandcontra-varianceinprogramminglanguages?[closed]编程语言中的协变和逆变有什么区别?[关闭]【发布时间】:2010-11-1221:41:19【问题描述】:谁能解释一... 查看详情

了解 C# 中的协变和逆变接口

】了解C#中的协变和逆变接口【英文标题】:UnderstandingCovariantandContravariantinterfacesinC#【发布时间】:2011-02-1218:42:53【问题描述】:我在我正在阅读的C#教科书中遇到了这些,但我很难理解它们,可能是由于缺乏上下文。对它们是... 查看详情

typescript中的协变和逆变(代码片段)

最近用TS时碰到协变和逆变的一些概念,发现有篇外国人写的文章比较容易理解的,这里记录下。1.协变和逆变简单理解先简单说下协变和逆变的理解。首先,无论协变还是逆变,必然是存在于有继承关系的类当中... 查看详情

如何检查函数中元素的协变和逆变位置?

...2018-07-2612:08:23【问题描述】:这是我读过的一篇关于scala中的逆变和协变的文章中的代码sn-p。但是,我无法理解scala编译器抛出的错误消息“错误:协变类型A发生在值pet2的类型A中的逆变位置classPets[ 查看详情

通用优先级队列中的协变和逆变类型

】通用优先级队列中的协变和逆变类型【英文标题】:Co-andcontravarianttypesingenericpriorityqueue【发布时间】:2010-07-3017:10:01【问题描述】:我正在尝试在Scala中实现一个在类型T上参数化的通用数据类型,它应该是Ordered[T]。具体来说... 查看详情

为啥 C# (4.0) 不允许泛型类类型中的协变和逆变?

】为啥C#(4.0)不允许泛型类类型中的协变和逆变?【英文标题】:WhydoesC#(4.0)notallowco-andcontravarianceingenericclasstypes?为什么C#(4.0)不允许泛型类类型中的协变和逆变?【发布时间】:2021-12-0301:18:42【问题描述】:造成这种限制的真正... 查看详情

详解c#的协变和逆变(代码片段)

  一、使用协变(Covariance)和逆变(Contravariance)能够实现数组之间、委托实例和方法之间、泛型委托实例之间、泛型接口的变量和泛型类型的对象之间、泛型接口的变量之间的隐式转换;使用协变将允许使用比原指定类型派... 查看详情

typescript中的协变和逆变(代码片段)

最近用TS时碰到协变和逆变的一些概念,发现有篇外国人写的文章比较容易理解的,这里记录下。1.协变和逆变简单理解先简单说下协变和逆变的理解。首先,无论协变还是逆变,必然是存在于有继承关系的类当中... 查看详情

c#泛型的协变和逆变

例:(引用自JobLog的博客:http://blog.csdn.net/baidu_20993761/article/details/47272597)publicdelegateTResultFunc<outTResult>();//TResult类型是返回值类型publicdelegateTResultFunc<inT,outTResult>(Targ);//输入类型 查看详情

scala的协变covariant(+),逆变contravariant(-),上界(<:),下界(;:)

原文:https://my.oschina.net/xinxingegeya/blog/486671Scala的协变(+),逆变(-),上界(<:),下界(>:)协变covariant、逆变contravariant、不可变invariant对于一个带类型参数的类型,比如List[T],如果对A及其子类型B,... 查看详情