系列中的上一篇
当前教程
类型推断
系列中的下一篇

系列中的上一篇: 泛型简介

系列中的下一篇: 通配符

类型推断

 

类型推断和泛型方法

类型推断是 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.Runnablejava.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 日


系列中的上一篇
当前教程
类型推断
系列中的下一篇

系列中的上一篇: 泛型简介

系列中的下一篇: 通配符