逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)
里氏替换原则的内容可以描述为: “派生类(子类)对象可以在程序中代替其基类(超类)对象。”
通俗来讲,协变与逆变是描述,类型转换前继承关系和类型转化后继承关系的变化的。 此处的类型转化在java中最常见的就是泛型类。 继承关系最明显的作用就是里式替换,当一个类A可以在程序中代替另一个类B,那么A就是B的子类。
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类的元素,所以②处执行时会异常,但是可以正常编译。
如果数组不协变,入参为 Fruit[]
类型的地方不能传入 Apple[]
,数组使用的时候会丧失多态的灵活性。
但是多态带来了新的问题,引用类型( Fruit[]
)和实际类型( Apple[]
)不相同,导致Apple[]可能会被放入Fruit元素。
如果 Apple[]
数组不管不顾,将Fruit对象接受,那可能导致从 Apple[]
数组中读取一个非Apple对象的Fruit对象,这是不符合数组定义的(数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构)。
所幸的是,数组在接受对象时会做类型检查。所以不会出现上述情况。
综上,数组协变带来了灵活性,又因为类型检查,数组依然是安全的,所以数组设计成了协变的。
一般情况下,如果类A ≤ B,那么泛型类 T<A>
和泛型类 T<B>
没什么明确的继承关系。
一句话总结,正常情况下泛型是不变的。
如果泛型始终是不变的,那么所有的泛型类之间没有任何继承关系,那泛型类就完全没有多态性,泛型存在的意义也会大幅削弱。 所以java提供了泛型约束符来实现逆变与协变。
泛型逆变,即,如果类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关键字可以实现 逆变
。
泛型协变,即,如果类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关键字可以实现 协变
。
本文的重点在于 协变 与 逆变 ,列举了数组、泛型的例子来说明协变逆变的概念。 泛型化类或者数组,都会带来转化后的类(泛型类或数组)与原始类之间继承关系如何确定的问题。 协变与逆变这一组概念的提出就是为了确定转化后的类的继承关系。 很多语言实现泛型时其实都要考虑这个问题,据我所知C#和kotlin等语言采用了和java类似的方式。
水平有限,难免有错漏之处。有问题请联系我(cxkun992@gmail.com),大家一起讨论。