Java的协变,逆变与不变
目录
在Java里,如果我们创建了Integer类和Number类,并且Integer是Number的子类。那么List<Integer>类也是List<Number>类的子类吗?
我们可以写一段代码来找到这个问题的答案。现在我们有一个求List<Number>类最大值的方法,它的输入是List<Number>类的实例。若List<Integer>类也是List<Number>类的子类,那么下面的代码应该是合法的。
1 | public class InvariantExample { |
但是编译器告诉我们incompatible types错误,这里max方法期待一个List<Number>类作为输入,但是实际上获得的是List<Integer>类。这就告诉我们,即便Integer是Number的子类,编译器也认为List<Integer>类和List<Number>类是没有关系的。
结论:对于任意两个不同的类型Type1 和 Type2,不论它们之间具有什么关系,List<Type1> 和 List<Type2>都是没有关系的。
如果我们把上述结论推广,可以得到更一般性的结论
对于任意两个不同的类型Type1 和 Type2,不论它们之间具有什么关系,给定泛型类 G<T>, G<Type1> 和 G<Type2>都是没有关系的。
上面的结论可以用一句话概括:
Java的泛型是不可变的(Invariant)
协变,逆变与不变的定义
在一门程序设计语言的类型系统中,给定
<=和>=, 表示类型排序关系Type1<=Type2表示Type1是Type2的子类型Type1>=Type2表示Type1是Type2的超类型
f()表示一个类型的构造函数- 注意它可以是一元函数(接受一个参数),如
NewType = f(Type) - 也可以是多元函数(接受多个参数),如
NewType = f(Type1, Type2)
- 注意它可以是一元函数(接受一个参数),如
那么,对于任意两个不同的类型Type1 和 Type2, 这个类型的构造函数f()可以是
| 变型(Variance) | 定义 | 例子 |
|---|---|---|
| 协变的(covariant) | 当且仅当它保持了类型排序关系 | 如果Type1<=Type2,那么f(Type1)<=f(Type2) |
| 逆变的(contravariant) | 当且仅当它逆转了类型排序关系 | 如果Type1<=Type2,那么f(Type1)>=f(Type2) |
| 不变的(invariant or nonvariant) | 既不保持也不逆转类型排序关系 | 不论Type1 和 Type2的关系,f(Type1) 和f(Type2) 没有关系 |
以上定义来自 Covariance and contravariance, Wikipedia,其中还有双变的(bivariant),但是不在本文的讨论范围,顾略去
在 C#中,子类型也称为派生类(Derived class),超类型也称为基类(Base class)
不同的程序设计语言,需要在类型推断的安全性和语法的易用性上做出权衡,对不同的类型构造函数进行规约,以使其符合某一种变型(Variance)。
不同的程序设计语言,在支持变型时,也有不同的处理方式。例如C#只允许在接口类型上标记变型,,而Java则在通过通配符来支持变型。
Java上限通配符实现协变
继续最初的例子,Integer是Number的子类,怎么样才能让Number max(List<Number> list)max方法也接受List<Integer>呢?(这里List<T>是类型构造函数,List<Integer> 和 List<Number> 两个新的类型)虽然在Java中,泛型是不可变的(List<Integer> 和 List<Number>没有关系),但是Java可以通过使用上限通配符来实现协变。
修改后的max方法签名如下
1 | public static Number max(List<? extends Number> list) |
? extends Number 表示接受Number类型和Number类型的子类型。这样max方法能够接受比原始指定的类型(这里是Number类型)更具体的类型。
Java下限通配符实现逆变
思考下面的例子,Integer是Number的子类,方法generateIntegers生成List<Integer> 并放入到输入参数output中
1 | public class ContravariantExample { |
由于Java泛型的不可变性,generateIntegers() 生成的List<Integer>不能放到输入参数output指定的List<Number>中。那么有没有办法把 List<Integer> 放到List<Number>中呢?
这里使用下限通配符来实现逆变。
修改后的generateIntegers函数签名如下
1 | public static void generateIntegers(List<? super Integer> output) |
<? super Integer 表示接受Integer类型和Integer类型的超类型。这样generateIntegers方法就能接受比原始指定类型(这里是Integer类型)更泛化(更不具体)的类型。
总结
如果Integer类是Number类的子类型
| 变型(Variance) | 通配符 | 例子 |
|---|---|---|
| 协变的(covariant) | 上限通配符 ? extends T |
List<? extends Number> 接受比原始指定类型Number更具体的类型 |
| 逆变的(contravariant) | 下限通配符 ? super T |
List<? super Integer> 接受比原始指定类型Integer更泛化的类型 |
| 不变的(invariant or nonvariant) | 泛型默认不可变 | List<Integer> 和List<Number> 没关系 |
- Covariance and contravariance, Wikipedia
- 泛型中的协变和逆变, Microsoft .NET指南
- Effective Java 3rd Edition,Joshua Bloch