Java的协变,逆变与不变

目录

在Java里,如果我们创建了Integer类和Number类,并且IntegerNumber的子类。那么List<Integer>类也是List<Number>类的子类吗?

我们可以写一段代码来找到这个问题的答案。现在我们有一个求List<Number>类最大值的方法,它的输入是List<Number>类的实例。若List<Integer>类也是List<Number>类的子类,那么下面的代码应该是合法的。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class InvariantExample {
static class Number { }
static class Integer extends Number { }

public static Number max(List<Number> list) {
// 省略实现细节
}

public static void main(String[] args) {
final List<Integer> integerList = new ArrayList<>();
final Number max = max(integerList); // 编译错误 incompatible types
}
}

但是编译器告诉我们incompatible types错误,这里max方法期待一个List<Number>类作为输入,但是实际上获得的是List<Integer>类。这就告诉我们,即便IntegerNumber的子类,编译器也认为List<Integer>类和List<Number>类是没有关系的。

结论对于任意两个不同的类型Type1Type2,不论它们之间具有什么关系,List<Type1>List<Type2>都是没有关系的。

如果我们把上述结论推广,可以得到更一般性的结论

对于任意两个不同的类型Type1Type2,不论它们之间具有什么关系,给定泛型类 G<T>G<Type1>G<Type2>都是没有关系的。

上面的结论可以用一句话概括:

Java的泛型是不可变的(Invariant)

协变,逆变与不变的定义

在一门程序设计语言的类型系统中,给定

  • <=>=, 表示类型排序关系
    • Type1<=Type2表示Type1Type2的子类型
    • Type1>=Type2表示Type1Type2的超类型
  • f() 表示一个类型的构造函数
    • 注意它可以是一元函数(接受一个参数),如NewType = f(Type)
    • 也可以是多元函数(接受多个参数),如NewType = f(Type1, Type2)

那么,对于任意两个不同的类型Type1Type2, 这个类型的构造函数f()可以是

变型(Variance) 定义 例子
协变的(covariant) 当且仅当它保持了类型排序关系 如果Type1<=Type2,那么f(Type1)<=f(Type2)
逆变的(contravariant) 当且仅当它逆转了类型排序关系 如果Type1<=Type2,那么f(Type1)>=f(Type2)
不变的(invariant or nonvariant) 既不保持也不逆转类型排序关系 不论Type1Type2的关系,f(Type1)f(Type2) 没有关系

以上定义来自 Covariance and contravariance, Wikipedia,其中还有双变的(bivariant),但是不在本文的讨论范围,顾略去

在 C#中,子类型也称为派生类(Derived class),超类型也称为基类(Base class)

不同的程序设计语言,需要在类型推断的安全性和语法的易用性上做出权衡,对不同的类型构造函数进行规约,以使其符合某一种变型(Variance)
不同的程序设计语言,在支持变型时,也有不同的处理方式。例如C#只允许在接口类型上标记变型,,而Java则在通过通配符来支持变型。

Java上限通配符实现协变

继续最初的例子,IntegerNumber的子类,怎么样才能让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下限通配符实现逆变

思考下面的例子,IntegerNumber的子类,方法generateIntegers生成List<Integer> 并放入到输入参数output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ContravariantExample {
static class Number { }
static class Integer extends Number { }

public static void generateIntegers(List<? super Integer> output) {
// 省略实现细节
}

public static void main(String[] args) {
final List<Number> numberList = new ArrayList<>();
generateIntegers(numberList); // 编译错误 incompatible types
}
}

由于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> 没关系
  1. Covariance and contravariance, Wikipedia
  2. 泛型中的协变和逆变, Microsoft .NET指南
  3. Effective Java 3rd Edition,Joshua Bloch