Oct 23, 2017
Java 泛型-类型推断
类型推断(Type Inference)是指 Java 编译器能查看每个方法的调用和相应声明,以确定调用合适的类型参数(Type Argument)或参数。推断算法决定参数的类型,如果可用,则指定被赋值的类型或返回的类型。最后,推断算法试图在一起工作的所有参数中找到最具体的类型。
为了说明最后一点,在下面的例子中,泛型推断确定将传入 pick 方法的第二个参数作为 Serializable 的类型:
1 | static <T> T pick(T a1, T a2) { |
类型推断和泛型方法
引入类型推断的泛型方法,能够让你像调用普通方法一样调用泛型方法,而不需要在尖括号中指定类型。思考下面的例子,BoxDemo,它需要 Box 类。
1 | public class BoxDemo { |
下面是这个例子的输出结果:
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
泛型方法 addBox 定义了一个名叫 U 的类型参数。一般 Java 编译器能推断泛型方法调用的类型参数。因此,大多数情况下,你不需要指定类型参数。例如,为了调用泛型方法 addBox,你可以使用类型证据指定一个类型参数:
1 | BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes); |
或者,你可以省略类型证据,Java 编译器能从传入泛型方法的参数中自动推断出类型参数为 Integer:
1 | BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes); |
泛型类的实例化
只要编译器能从上下文中推断出类型参数,我们就可以使用空的类型参数(棱形)来替换类型参数去调用泛型类的构造方法。
例如,思考下面的变量声明:
1 | Map<String, List<String>> myMap = new HashMap<String, List<String>>(); |
我们可以使用空的类型参数(棱形)来代替构造方法的参数化类型:
1 | Map<String, List<String>> myMap = new HashMap<>(); |
注意,为了在泛型类实例化过程中使用类型推断,你必须使用棱形。下面的例子中,因为 HashMap() 的构造方法引用了 HashMap 原始类型而不是 HashMap<String, List<String>>
,所以编译器生成一个 unchecked conversion
的警告。
1 | Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning |
泛型构造方法
注意,泛型类或非泛型类的构造方法可以是泛化的(换句话说是声明它们自己的类型参数)。思考下面的例子:
1 | class MyClass<X> { |
思考下面 MyClass 类的实例:
1 | new MyClass<Integer>("") |
这个语句创建一个参数化类型为 MyClass<Integer>
的实例,也明确指定 MyClass<X>
中类型参数 X 是 Integer 类型。注意,这个泛型类的构造方法包含一个类型参数 T。编译器推断出这个泛型类的构造方法的类型参数 T 是 String 类型,因为构造器的实际参数是一个 String 对象。
在 Java SE 7 之前发布的编译器能够推断泛型构造方法的实际类型参数,类似于泛型方法。但是,Java SE 7 和后面的编译器可以推断出使用了棱形的泛型类的实际类型参数:
1 | MyClass<Integer> myObject = new MyClass<>(""); |
在这个例子中,编译器能推断出泛型类 MyClass<X>
中 X 是 Integer 类型,泛型构造方法中 T 是 String 类型。
需要注意的是,类型推断算法仅适用调用参数、目标类型,以及可能明显的预期返回类型来推断类型。类型推断算法不使用后面程序中的结果。
目标类型
Java 编译器利用目标类型来推断出泛型方法调用的类型参数。表达式的目标类型是 Java 编译器所期望的数据类型,这取决于表达式出现的位置。思考下面声明的 Collections.emptyList 方法:
1 | static <T> List<T> emptyList(); |
思考下面的赋值语句:
1 | List<String> listOne = Collections.emptyList(); |
这个语句期望得到一个 List<String>
实例,List<String>
是目标类型。因为 emptyList 方法返回一个 List<T>
类型的值,编译器推断出类型参数 T 一定是一个 String。这在 Java SE 7 或 8 中都有效。或者我们可以使用类型证据并指定 T 的值如下:
1 | List<String> listOne = Collections.<String>emptyList(); |
在这个上下文中,我们并不需要这么做。不过在其他上下文中是需要的,思考下面的方法:
1 | void processStringList(List<String> stringList) { |
假设你想使用一个 emptyList 来调用 processStringList 方法,在 Java SE 7 中,下面的语句是编译不通过的:
1 | processStringList(Collections.emptyList()); |
Java SE 7 编译会生成一个错误类似如下:
1 | List<Object> cannot be converted to List<String> |
编译器需要类型参数 T 的值,所以 T 从 Object 开始。因此,调用 Collections.emptyList 会返回 List<Object>
,这个与 processStringList 方法不兼容。所以在 Java SE 7 中,我们必须制定类型参数:
1 | processStringList(Collections.<String>emptyList()); |
在 Java SE 8 中已经不需要这样。例如 processStringList 方法的参数,目标类型已经被扩展到包含方法参数。在这种情况下,processStringList 方法需要一个 List<String>
类型的参数,Collections.emptyList 方法返回一个 List<T>
类型的值,所以使用 List<String>
的目标类型,编译器推断类型参数 T 具有一个 String 的值。所以在 Java SE 8 中,下面的语句能编译通过:
1 | processStringList(Collections.emptyList()); |