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