逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
- f(⋅)是逆变(contravariant)的,当A ≤ B时有f(B) ≤ f(A)成立;
- f(⋅)是协变(covariant)的,当A ≤ B时有f(A) ≤ f(B)成立;
- f(⋅)是不变(invariant)的,当A ≤ B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。
1.2 里式替换原则
里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程序中代替其基类(超类)对象。”
1.3 解释
通俗来讲,协变与逆变是描述,类型转换前继承关系和类型转化后继承关系的变化的。
此处的类型转化在java中最常见的就是泛型类。
继承关系最明显的作用就是里式替换,当一个类A可以在程序中代替另一个类B,那么A就是B的子类。
2.java数组
2.1 数组协变
java数组是协变的。即,如果有类型A ≤ B,则数组类型A[]与数组类型B[]之间也有,A[] ≤ B[]。
示例如下:
public class Tmp { static class Fruit { public void name() { System.out.println("fruit"); } } static class Apple extends Fruit { public void name() { System.out.println("apple"); } } static class GreenApple extends Apple { public void name() { System.out.println("green apple"); } } @Test public void test() { Fruit[] fruits = new Apple[10]; // ① fruits[0] = new Apple(); fruits[1] = new Apple(); fruits[2] = new Fruit(); // ② throw java.lang.ArrayStoreException } } 复制代码
上面的例子中Apple继承自Fruit,所以数组类型 Apple[]
也是 Fruit[]
的子类,所以①处是合法的,编译器不会报错,也可以正常运行。
但是 new Apple[10]
中只能放入Apple类的元素,所以②处执行时会异常,但是可以正常编译。
2.2 数组协变的原因
如果数组不协变,入参为 Fruit[]
类型的地方不能传入 Apple[]
,数组使用的时候会丧失多态的灵活性。
但是多态带来了新的问题,引用类型( Fruit[]
)和实际类型( Apple[]
)不相同,导致Apple[]可能会被放入Fruit元素。
如果 Apple[]
数组不管不顾,将Fruit对象接受,那可能导致从 Apple[]
数组中读取一个非Apple对象的Fruit对象,这是不符合数组定义的(数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构)。
所幸的是,数组在接受对象时会做类型检查。所以不会出现上述情况。
综上,数组协变带来了灵活性,又因为类型检查,数组依然是安全的,所以数组设计成了协变的。
3.java泛型
3.1 泛型不变
一般情况下,如果类A ≤ B,那么泛型类 T<A>
和泛型类 T<B>
没什么明确的继承关系。
一句话总结,正常情况下泛型是不变的。
如果泛型始终是不变的,那么所有的泛型类之间没有任何继承关系,那泛型类就完全没有多态性,泛型存在的意义也会大幅削弱。
所以java提供了泛型约束符来实现逆变与协变。
3.2 泛型逆变
泛型逆变,即,如果类A ≤ B,那么 T<B>
≤ T<A>
。
java泛型使用super关键字实现逆变。
public class Tmp {
static class Fruit {
public void name() {
System.out.println("fruit");
}
}
static class Apple extends Fruit {
public void name() {
System.out.println("apple");
}
}
static class GreenApple extends Apple {
public void name() {
System.out.println("green apple");
}
}
@Test
public void test1() {
List<Fruit> fruits = Arrays.asList(new Fruit(), new Fruit(), new Apple());
eat(fruits); // ① 正常调用
List<GreenApple> greenApples = Arrays.asList(new GreenApple());
eat(greenApples); // ② 语法错误
}
private void eat(List<? super Apple> list) {
System.out.println("eat");
}
}
复制代码
上例中①处正常调用,说明 List<Fruit>
是 List<? super Apple>
的子类,所以使用super关键字可以实现 逆变
。
3.3 泛型协变
泛型协变,即,如果类A ≤ B,那么 T<A>
≤ T<B>
。
java泛型使用extends关键字实现逆变。
public class Tmp { static class Fruit { public void name() { System.out.println("fruit"); } } static class Apple extends Fruit { public void name() { System.out.println("apple"); } } static class GreenApple extends Apple { public void name() { System.out.println("green apple"); } } @Test public void test1() { List<Fruit> fruits = Arrays.asList(new Fruit(), new Fruit(), new Apple()); eat(fruits); // ① 语法错误 List<GreenApple> greenApples = Arrays.asList(new GreenApple()); eat(greenApples); // ② 正常执行 } private void eat(List<? extends Apple> list) { System.out.println("eat"); } } 复制代码
上例中②处正常调用,说明 List<GreenApple>
是 List<? extends Apple>
的子类,所以使用super关键字可以实现 协变
。
4.总结
本文的重点在于 协变
与 逆变
,列举了数组、泛型的例子来说明协变逆变的概念。
泛型化类或者数组,都会带来转化后的类(泛型类或数组)与原始类之间继承关系如何确定的问题。
协变与逆变这一组概念的提出就是为了确定转化后的类的继承关系。
很多语言实现泛型时其实都要考虑这个问题,据我所知C#和kotlin等语言采用了和java类似的方式。
水平有限,难免有错漏之处。有问题请联系我(cxkun992@gmail.com),大家一起讨论。
原文
https://juejin.im/post/5ef3649bf265da22b8557653
本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » java中的协变与逆变