关于Java的协变和逆变

逆变与协变:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类):

  1. f(⋅)是逆变(contravariant)的,当A≤B时有f(B)≤f(A)成立;
  2. f(⋅)是协变(covariant)的,当A≤B时有f(A)≤f(B)成立;
  3. f(⋅)是不变(invariant)的,当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

1.数组的协变性

在Java中数组是具有协变性的,如果B是A的子类型,则B[]是A[]的子类型(f(⋅)映射为数组),即子类型的数组可以赋予父类型的数组进行使用,但数组的类型实际为子类型。例如:

Fruit[] fruits = new Apple[10]; // subclass of fruits
fruits[0] = new Apple();
fruits[1] = new RedFujiApple(); // subclass of Apple

这里fruits所引用的数组其实是Apple[]类型。

从协变数组读取元素是完全安全的,无论是编译期还是运行时,都不会发生任何问题:

Fruit fruit = fruits[0]; // return an Apple, which is the subclass of Fruit

但是将Fruit类型以及其子类型的元素写入到协变数组fruits中是有可能在运行时出现问题的,因为Apple类型无接受Fruit类型和其它非Apple的子类型(编译器无法检查):

fruits[0] = new Fruit(); // java.lang.ArrayStoreException
fruits[0] = new Orange(); //subclass of Fruit, java.lang.ArrayStoreException

这是Java数组的“缺陷”,在利用数组的协变性时,应该尽量把协变数组当作只读数组使用。

2.泛型中的协变和逆变

Java中泛型是不变的,但可以通过通配符"?"实现协变和逆变:

  1. 实现了泛型的协变:
List<? extends Number> list = new ArrayList<Integer>();
  1. 实现了泛型的逆变:
List<? super Integer> list = new ArrayList<Number>();

由于泛型的协变只能规定类的上界,逆变只能规定下界,使用时需要遵循PECS(producer-extends, consumer-super): 要从泛型类取数据时,用extends; 要往泛型类写数据时,用super; 既要取又要写,就不用通配符(即extends与super都不用)。

3.函数替换:
函数f可以安全替换函数g,要求函数f接受更一般的参数类型,返回更特化的结果类型(输入类型是逆变,输出类型协变)。
由LSP原则(Liskov Substitution Principle),即所有引用父类型的地方必须能透明地使用其子类型的对象。为了安全的替换替换函数g,我们需要用原有参数类型或其父类接受客户的传入,返回原有类型的子类。以此遵循对应的规格说明。

你可能感兴趣的:(关于Java的协变和逆变)