`

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

 
阅读更多
原文:http://jerrypeng.me/2013/02/java-generics-invariant-and-inference/

下面以这段短小的代码来作为例子解释:
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 处。但为什么会这样呢?这要分两部分来说明:
泛型的“不协变(invariant)”问题
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 处加上以后编译立 刻能通过了。
剩下的问题是,为啥 list1 和 list3 两处可以通过编译?
方法调用的类型推断
方法调用的类型推断是个十分复杂的过程,对其完整的规则我还没有一个深入的 理解,说实话试图阅读 Java Language Specification 相关部分对我来说都十分 困难,感觉好难懂,有兴趣的读者可以自行查看相关章节(15.12.2.7 Inferring Type Arguments Based on Actual Arguments)。
不过对于上面那个简单的例子,我可以得出一个比较 naive 的结论:
对于某个泛型方法 M 中包含的泛型参数 T1..Tn,Java 编译器会根据调用上下文
(Calling Context),包括实际参数和返回值等,推断出尽可能“具体”的实际
类型。
另外还有一条我还不太确定的结论:如果一个泛型参数同时出现在参数和返回值 中,则类型推断以参数为准,仅当不包含泛型参数的时候才会参考函数返回值。
看一下上面的例子, list1 , list2 , list3 看起来差不多,为什么 只有 list2 处编译不通过?我们可以结合上面提到的规则看一下:
根据 list1 处的两个参数 AppleTree.class 和 BananaTree.clas 可 以推断出来的最“具体”的类型是 List<Class<? extends Tree>> ,和前面 list1 的声明完全吻合,所以不受不协变的影响,是合法的。
根据 list3 处的三个参数 AppleTree.class , BananaTree.class 和 Grass.class 可以推断出来的最“具体”的类型是 List<Class<? extends Plant>> ,和 list3 的声明也完全吻合,同理也是合法的。
list2 处推断出来的是 List<Class<? extends Tree>> ,和前面声明的 List<Class<? extends Plant>> 不兼容,所以编译报错。
再回到最开始那个例子,其中的 Module 是 Google Guice 的一个接口,而下 面那句 newHashSet 调用推断出来的是 Set<Class<? extends AbstractModule>> ,所以会报错。只要相应地把方法返回值改成 Set<? extends Class<? extends Module>> 即可解决我最初的问题。
分享到:
评论

相关推荐

    java泛型的内部原理及更深应用

    10. **Erasure和类型安全**:尽管类型擦除可能让人感觉泛型在运行时失去了作用,但实际上,编译器通过类型擦除实现了类型安全。它会在编译期间检查泛型的使用,防止不兼容类型的对象被放入泛型容器中。 通过学习...

    Java泛型_Java中的泛型结构_

    - 但是,泛型不支持协变(Covariance)和逆变(Contravariance),所以 `List&lt;Dog&gt;` 不是 `List&lt;Animal&gt;` 的子类型。 8. 类型推断(Type Inference): - 自Java 7起,编译器可以自动推断泛型的类型,例如在创建...

    [Java泛型和集合].(Java.Generics.and.Collections).文字版

    6. **协变和逆变**:理解如何在泛型中实现类型参数的协变和逆变。 7. **比较和equals**:在泛型上下文中正确地实现Comparable和equals()方法。 8. **枚举类型与泛型**:结合使用枚举和泛型来增强类型安全。 通过...

    java泛型精华

    Java泛型的实现方式被称为“擦除”(Erasure),即在编译阶段,编译器会根据泛型参数进行类型检查和推断,但最终生成的字节码并不包含泛型信息。这意味着,如`List&lt;Integer&gt;`和`List&lt;String&gt;`在运行时实际上是同一个...

    Java 理论和实践 了解泛型

    8. 对于数组,由于历史原因,Java的泛型不支持通配符数组,这意味着你不能创建一个`Number[]`类型的数组并将其赋值给`List&lt;Number&gt;`,因为数组是固定类型的,而泛型列表是协变的。因此,通常需要将数组转换为`List`...

    java泛型,java知识

    Java泛型是JDK 1.5引入的重要特性,它为Java编程提供了类型安全的集合框架,使得在编译时期就能进行类型检查,避免了运行时的类型转换风险,极大地提高了代码的可读性和健壮性。泛型的引入是为了在不牺牲面向对象...

    Java泛型深入研究

    4. **类型擦除**:Java泛型在编译后会进行类型擦除,这意味着在运行时所有的泛型信息都会丢失,泛型只在编译时起作用,用于增强类型安全。 5. **协变与逆变**:泛型的协变(Covariance)和逆变(Contravariance)...

    Java泛型学习笔记.pdf

    Java泛型是Java语言提供的一种编程特性,旨在支持在编译时期进行类型检查和类型消除,使得编写的代码在不放弃类型安全的前提下,具有更好的通用性和复用性。学习Java泛型能够帮助我们更好地编写和使用通用的类、接口...

    java中的泛型样例

    7. **协变与逆变**:在Java中,泛型是协变的,意味着`List&lt;Number&gt;`是`List&lt;Object&gt;`的子类型,这允许将Number列表赋值给Object列表。然而,对于泛型的参数类型,它是逆变的,例如`List&lt;? super Integer&gt;`可以接受...

    java-generic.rar_泛型

    10. **协变与逆变**:泛型的协变和逆变涉及到类型参数的位置以及它们如何影响方法签名的兼容性。例如,`List&lt;? extends Number&gt;` 是协变的,因为它允许更具体的子类型作为参数;而 `Comparator&lt;? super T&gt;` 是逆变的...

    Java源码泛型的集合类应用.rar

    6. **原始类型与类型擦除**:Java泛型是通过类型擦除实现的,这意味着在运行时所有的泛型信息都会被移除。为了保持向后兼容,Java允许使用未指定类型的原始类型(如`List`而非`List&lt;String&gt;`),但这样会失去泛型...

    泛型.rar

    泛型的协变和逆变是关于类型参数在作为方法返回类型和参数类型时的行为。简单来说,如果类型参数在方法返回类型中使用,那么它是协变的;如果在方法参数中使用,它是逆变的。了解这些概念有助于编写更加灵活和安全...

    使用通配符简化泛型使用1

    通配符是Java泛型中一个重要的工具,用于处理未知类型或类型范围。它们在保持类型安全的同时增加了代码的灵活性。了解如何恰当地使用通配符可以帮助开发人员编写更安全、更健壮的Java代码。在实际编程中,正确理解和...

    JDK1.5的泛型实现

    Java泛型是自JDK 1.5版本引入的一项重要特性,它极大地提高了代码的类型安全性和重用性。泛型允许我们在定义类、接口和方法时指定参数化类型,这样在编译时期就能检查类型匹配,避免了运行时的类型转换异常。以下是...

    JDK1.5泛型.rar

    **Java泛型是JDK1.5引入的一个重要特性,极大地提高了代码的类型安全性和重用性。在泛型出现之前,程序员需要在运行时进行强制类型转换,这可能导致ClassCastException。泛型通过在类、接口和方法声明中引入类型参数...

    java泛型源码-generic_samples:Pong.java源代码,作为并发利用JavaAPI的一部分

    6. **协变与逆变:**Java泛型支持协变和逆变,这是在处理泛型接口和类时的重要特性。协变允许子类型替换父类型,而逆变则允许父类型替换子类型。在并发编程中,了解这些概念有助于编写更灵活和安全的代码。 7. **...

    java泛型学习示例

    Java泛型是编程语言中一个强大的工具,它允许开发者在编写代码时定义具有类型参数的类、接口和方法。在JDK 5引入后,泛型显著提高了代码的类型安全性和可读性,减少了类型转换的需要,并且帮助避免了运行时的...

    java泛型源码-Java-Generics-Our-Generics-Class-Part-3-Source-code:通用课程

    8. **类型参数的协变和逆变**:在处理泛型集合时,了解协变和逆变的概念非常重要。比如,`List&lt;Number&gt;`是`List&lt;Object&gt;`的子类型,这称为协变;而`List&lt;? extends Number&gt;`可以赋值给`List&lt;Number&gt;`,这称为逆变。 ...

    第1章 泛型+ppt+pdf+例子

    泛型的协变和逆变也是重要概念。在Java中,集合类的读操作是协变的,意味着`List&lt;Number&gt;`可以被赋值给`List&lt;Object&gt;`,因为Number是Object的子类。然而,写操作是逆变的,`List&lt;Object&gt;`不能被赋值给`List&lt;Number&gt;`...

    CSharpt ToJ ava Converter 22.4.20 完美版

    6. **泛型**:两种语言都支持泛型,但C#的泛型更灵活,允许协变和逆变,Java泛型则是非协变的。 7. **接口与抽象类**:Java只允许单继承,但可以多实现接口;C#允许单一的基类和多接口实现。转换时,需要根据具体...

Global site tag (gtag.js) - Google Analytics