小探 Java 泛型系统的不协变问题和类型推断

这个问题起源于最近在生产代码中写下的这样一个方法:

protected Set<Class<? extends Module>> getAppModuleClasses() {
    return Sets.<Class<? extends Module>> newHashSet(DAOModule.class,
                                                     ACSModule.class,
                                                     TR069Module.class,
                                                     STUNModule.class);
}

那句显式的类型参数实在是太碍眼了,但是去掉它则会导致编译无法通过。在忍 受了这段代码一段时间后,我决定对这个问题好好研究一下。

下面以这段短小的代码来作为例子解释:

static interface Plant {}
static class Grass implements Plant {}
static class Tree implements Plant {}
static class AppleTree extends Tree {}
static class BananaTree extends Tree {}

public static void main(String[] args) {
    List<Class<? extends Tree>> list1
         = Arrays.asList(AppleTree.class, BananaTree.class);
    List<Class<? extends Plant>> list2
         = Arrays.asList(AppleTree.class, BananaTree.class);
    List<Class<? extends Plant>> list3 
         = Arrays.asList(AppleTree.class,  BananaTree.class, Grass.class);
    List<? extends Class<? extends Plant>> list4
         = Arrays.asList(AppleTree.class,  BananaTree.class);
}

上面的代码编译无法通过,读者可以猜测一下是哪一处有问题。

答案是 list2 处。但为什么会这样呢?这要分两部分来说明:

  1. 泛型的“不协变(invariant)”问题
  2. Java 泛型方法调用的类型推断

Java 泛型的“不协变”问题

其实这个问题是所有接触到 Java 泛型的人很快就会遇到的,应该属于很基础的 内容。Java 数组是协变(covariant)的,而泛型系统在不用 wildcard type 的 情况下是不协变的(invariant)1。比如可以把 Integer[] 赋值 Number[] ,但是不能把 List<Integer> 赋值给 List<Number>

但是当出现嵌套的泛型类型加上 wildcard type 时,我们还是容易迷 惑2。比如 List<Integer> 可以赋值给 List<? extends Number> ,那么 Set<List<Integer>> 是否可以赋值给 Set<List<? extends Number>> 呢?乍一看好像是可以的,但其实是不行的, 而我犯的就是这个错误。应该牢记,在不使用 wildcard type 的情况下泛型是不 协变的。虽然可以认为 List<Integer>List<? extends Number> 的子类 型,但 Set<List<Integer>> 不是 Set<List<? extends Number>> 的子类 型。为了解决这个问题,我们还是要加上 wildcard,把 Set<List<? extends Number>> 改成 Set<? extends List<? extends Number>> 即可解决问题。

说了这么多,其实就是一个简单的道理:想获得协变的效果,就要使用 wildcard 加 extends。

回到前面的例子, list2 那里编译不通过的原因,看一下错误信息,结合上 面的解释应该就很明了:

Type mismatch: cannot convert from List<Class<? extends TypeInference.Tree>> to List<Class<? extends TypeInference.Plant>>

在类型声明处加上 ? extends 就可以解决问题, list4 处加上以后编译立 刻能通过了。

剩下的问题是,为啥 list1list3 两处可以通过编译?

方法调用的类型推断

方法调用的类型推断是个十分复杂的过程,对其完整的规则我还没有一个深入的 理解,说实话试图阅读 Java Language Specification 相关部分对我来说都十分 困难,感觉好难懂,有兴趣的读者可以自行查看相关章节(15.12.2.7 Inferring Type Arguments Based on Actual Arguments)。

不过对于上面那个简单的例子,我可以得出一个比较 naive 的结论:

对于某个泛型方法 M 中包含的泛型参数 T1..Tn,Java 编译器会根据调用上下文
(Calling Context),包括实际参数和返回值等,推断出尽可能“具体”的实际
类型。

另外还有一条我还不太确定的结论:如果一个泛型参数同时出现在参数和返回值 中,则类型推断以参数为准,仅当不包含泛型参数的时候才会参考函数返回值。

看一下上面的例子, list1list2list3 看起来差不多,为什么 只有 list2 处编译不通过?我们可以结合上面提到的规则看一下:

  1. 根据 list1 处的两个参数 AppleTree.classBananaTree.clas 可 以推断出来的最“具体”的类型是 List<Class<? extends Tree>> ,和前面 list1 的声明完全吻合,所以不受不协变的影响,是合法的。
  2. 根据 list3 处的三个参数 AppleTree.classBananaTree.classGrass.class 可以推断出来的最“具体”的类型是 List<Class<? extends Plant>> ,和 list3 的声明也完全吻合,同理也是合法的。
  3. list2 处推断出来的是 List<Class<? extends Tree>> ,和前面声明的 List<Class<? extends Plant>> 不兼容,所以编译报错。

再回到最开始那个例子,其中的 Module 是 Google Guice 的一个接口,而下 面那句 newHashSet 调用推断出来的是 Set<Class<? extends AbstractModule>> ,所以会报错。只要相应地把方法返回值改成 Set<? extends Class<? extends Module>> 即可解决我最初的问题。

Comments