java中的协变与逆变
1.定义
1.1 基本概念
逆变与协变用来描述类型转换(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>
的子类,所以使用extends关键字可以实现协变。
4.总结
本文的重点在于协变与逆变,列举了数组、泛型的例子来说明协变逆变的概念。 泛型化类或者数组,都会带来转化后的类(泛型类或数组)与原始类之间继承关系如何确定的问题。 协变与逆变这一组概念的提出就是为了确定转化后的类的继承关系。 很多语言实现泛型时其实都要考虑这个问题,据我所知C#和kotlin等语言采用了和java类似的方式。
水平有限,难免有错漏之处。有问题请联系我(cxkun992@outlook.com),大家一起讨论。