类型推断

类型推断和泛型方法

类型推断是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();
      IO.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> 类型:

Map<String, List<String>> myMap = new HashMap(); // unchecked conversion warning

泛型与非泛型类的类型推断与泛型构造函数

请注意,构造函数既可以在泛型类中也可以在非泛型类中实现泛型(即声明自己的形式类型参数)。请看以下示例:

class MyClass<X> {
  <T> MyClass(T t) {
    // ...
  }
}

请考虑以下 MyClass 类的实例化:

new MyClass<Integer>("")

该语句创建了一个参数化类型 MyClass<Integer> 的实例;语句明确指定了泛型类 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> 类型的参数。方法 Collections.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,因此该lambda表达式属于此类型。这些方法所期望的数据类型称为目标类型。Java编译器通过求解lambda表达式所在上下文或情境的目标类型来确定其类型。由此可知,仅当Java编译器能确定目标类型时,才能在相应情境中使用lambda表达式:

  • 变量声明
  • 赋值语句
  • 返回语句
  • Array初始化表达式
  • 方法或者构造函数中的实参
  • Lambda表达式的主体
  • 条件表达式,三目运算符
  • 强制类型转换表达式

目标类型和方法实参

对于方法参数,Java编译器通过另外两个语言特性来确定目标类型:重载解析和类型参数推断。

考虑以下两个功能接口(java.lang.Runnablejava.util.concurrent.Callable<V>):

public interface Runnable {
    void run();
}

public interface Callable<V> {
    V call();
}

Runnable.run() 方法不返回值,而 Callable.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>