类型推断
类型推断和泛型方法
类型推断是 Java 编译器能够查看每个方法调用和相应的声明,以确定使调用适用的类型参数(或参数)。推断算法确定参数的类型,以及(如果可用)结果被分配或返回的类型。最后,推断算法尝试找到最具体的类型,该类型适用于所有参数。
为了说明最后一点,在以下示例中,推断确定传递给 pick 方法的第二个参数的类型为 Serializable
static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());
泛型方法向您介绍了类型推断,它使您能够像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。考虑以下示例 BoxDemo
,它需要 Box
类
public class BoxDemo {
public static <U> void addBox(U u,
java.util.List<Box<U>> boxes) {
Box<U> box = new Box<>();
box.set(u);
boxes.add(box);
}
public static <U> void outputBoxes(java.util.List<Box<U>> boxes) {
int counter = 0;
for (Box<U> box: boxes) {
U boxContents = box.get();
System.out.println("Box #" + counter + " contains [" +
boxContents.toString() + "]");
counter++;
}
}
public static void main(String[] args) {
java.util.ArrayList<Box<Integer>> listOfIntegerBoxes =
new java.util.ArrayList<>();
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
BoxDemo.addBox(Integer.valueOf(30), listOfIntegerBoxes);
BoxDemo.outputBoxes(listOfIntegerBoxes);
}
}
以下是此示例的输出
Box #0 contains [10]
Box #1 contains [20]
Box #2 contains [30]
泛型方法 addBox()
定义了一个名为 U
的类型参数。通常,Java 编译器可以推断泛型方法调用的类型参数。因此,在大多数情况下,您不必指定它们。例如,要调用泛型方法 addBox()
,您可以使用类型见证指定类型参数,如下所示
BoxDemo.<Integer>addBox(Integer.valueOf(10), listOfIntegerBoxes);
或者,如果您省略类型见证,Java 编译器会自动推断(从方法的参数)类型参数为 Integer
BoxDemo.addBox(Integer.valueOf(20), listOfIntegerBoxes);
类型推断和泛型类的实例化
只要编译器可以从上下文中推断出类型参数,您就可以用空类型参数集 (<>
) 替换调用泛型类构造函数所需的类型参数。这对尖括号非正式地称为菱形。
例如,考虑以下变量声明
Map<String, List<String>> myMap = new HashMap<String, List<String>>();
您可以用空类型参数集 (<>
) 替换构造函数的参数化类型
Map<String, List<String>> myMap = new HashMap<>();
请注意,要利用泛型类实例化期间的类型推断,您必须使用菱形。在以下示例中,编译器会生成一个未经检查的转换警告,因为 HashMap()
构造函数引用的是 HashMap
原始类型,而不是 Map<String, List<String>>
类型
Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning
类型推断和泛型类和非泛型类的泛型构造函数
请注意,构造函数可以在泛型类和非泛型类中都是泛型的(换句话说,声明自己的形式类型参数)。考虑以下示例
class MyClass<X> {
<T> MyClass(T t) {
// ...
}
}
考虑以下 MyClass
类的实例化
new MyClass<Integer>("")
此语句创建参数化类型 MyClass<Integer>
的实例;该语句显式地为泛型 class MyClass<X>
的形式类型参数 X
指定了类型 Integer
。请注意,此泛型类的构造函数包含一个形式类型参数 T
。编译器会为该泛型类的构造函数的形式类型参数 T
推断出类型 String
(因为此构造函数的实际参数是 String
对象)。
Java SE 7 之前的版本中的编译器能够推断泛型构造函数的实际类型参数,类似于泛型方法。但是,Java SE 7 及更高版本中的编译器可以推断出实例化的泛型类的实际类型参数,如果您使用菱形 (<>
)。考虑以下示例
MyClass<Integer> myObject = new MyClass<>("");
在此示例中,编译器会为泛型类 MyClass<X>
的形式类型参数 X
推断出类型 Integer
。它会为该泛型类的构造函数的形式类型参数 T
推断出类型 String
。
注意:重要的是要注意,推断算法仅使用调用参数、目标类型以及可能明显的预期返回类型来推断类型。推断算法不使用程序中后面的结果。
目标类型
Java 编译器利用目标类型来推断泛型方法调用的类型参数。表达式的目标类型是 Java 编译器根据表达式出现的位置所期望的数据类型。考虑方法 Collections.emptyList()
,它声明如下
static <T> List<T> emptyList();
考虑以下赋值语句
List<String> listOne = Collections.emptyList();
此语句期望 List<String>
的实例,此数据类型是目标类型。因为方法 emptyList()
返回类型为 List<T>
的值,编译器会推断出类型参数 T
必须是值 String
。这在 Java SE 7 和 8 中都有效。或者,您可以使用类型见证并指定 T
的值,如下所示
List<String> listOne = Collections.<String>emptyList();
但是,在此上下文中没有必要。虽然在其他上下文中是必要的。考虑以下方法
void processStringList(List<String> stringList) {
// process stringList
}
假设您想使用空列表调用方法 processStringList()
。在 Java SE 7 中,以下语句无法编译
processStringList(Collections.emptyList());
Java SE 7 编译器会生成类似于以下内容的错误消息
List<Object> cannot be converted to List<String>
编译器需要类型参数 T
的值,因此它从值 Object
开始。因此,调用 Collections.emptyList()
返回类型为 List<Object>
的值,这与方法 processStringList()
不兼容。因此,在 Java SE 7 中,您必须指定类型参数的值,如下所示
processStringList(Collections.<String>emptyList());
这在 Java SE 8 中不再必要。目标类型的概念已扩展到包括方法参数,例如方法 processStringList()
的参数。在这种情况下,processStringList()
需要类型为 List<String>
的参数。方法 emptyList()
返回 List<T>
的值,因此使用 List<String>
的目标类型,编译器会推断出类型参数 T
的值为 String
。因此,在 Java SE 8 中,以下语句会编译
processStringList(Collections.emptyList());
Lambda 表达式中的目标类型
假设您有以下方法
public static void printPersons(List<Person> roster, CheckPerson tester)
以及
public void printPersonsWithPredicate(List<Person> roster, Predicate<Person> tester)
然后您编写以下代码来调用这些方法
printPersons(
people,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25);
以及
printPersonsWithPredicate(
people,
p -> p.getGender() == Person.Sex.MALE
&& p.getAge() >= 18
&& p.getAge() <= 25);)
您如何确定这些情况下 lambda 表达式的类型?
当 Java 运行时调用方法 printPersons()
时,它期望数据类型为 CheckPerson
,因此 lambda 表达式的类型为这种类型。但是,当 Java 运行时调用方法 printPersonsWithPredicate()
时,它期望数据类型为 Predicate<Person>
,因此 lambda 表达式的类型为这种类型。这些方法期望的数据类型称为目标类型。为了确定 lambda 表达式的类型,Java 编译器会使用找到 lambda 表达式的上下文或情况的目标类型。因此,您只能在 Java 编译器可以确定目标类型的环境中使用 lambda 表达式
- 变量声明
- 赋值
- 返回语句
- 数组初始化器
- 方法或构造函数参数
- Lambda 表达式主体
- 条件表达式,
?:
- 强制转换表达式
目标类型和方法参数
对于方法参数,Java 编译器使用其他两种语言特性来确定目标类型:重载解析和类型参数推断。
考虑以下两个函数接口 (java.lang.Runnable
和 java.util.concurrent.Callable<V>
public interface Runnable {
void run();
}
public interface Callable<V> {
V call();
}
方法 Runnable.run()
不返回值,而 Callable<V>.call()
则返回。
假设您已重载方法 invoke,如下所示(有关重载方法的更多信息,请参阅部分 定义方法)
void invoke(Runnable r) {
r.run();
}
<T> T invoke(Callable<T> c) {
return c.call();
}
以下语句将调用哪个方法?
String s = invoke(() -> "done");
将调用方法 invoke(Callable<T>)
,因为该方法返回值;方法 invoke(Runnable)
则不返回。在这种情况下,lambda 表达式 () -> "done"
的类型为 Callable<T>
。
上次更新: 2021 年 9 月 14 日