java泛型系列 | 六、类型推断

类型推断是 Java 编译器查看每个方法调用和相应声明以确定使调用适用的类型参数(或参数)的能力。 推理算法确定参数的类型,以及分配或返回结果的类型。

最后,推理算法尝试找到适用于所有参数的最具体的类型。为了说明最后一点,在以下示例中,推理确定传递给 pick 方法的第二个参数是 Serializable 类型:

static <T> T pick(T a1, T a2) { return a2; }
Serializable s = pick("d", new ArrayList<String>());

1、类型推断和泛型方法

泛型方法向您介绍了类型推断,它使您可以像调用普通方法一样调用泛型方法,而无需在尖括号之间指定类型。 考虑以下示例 BoxDemo,它需要 Box 类:

public class BoxDemo {

  public static <U> void addBox(U u, List<Box<U>> boxes) {
    Box<U> box = new Box<>();
    box.set(u);
    boxes.add(box);
  }

  public static <U> void outputBoxes(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) {
    ArrayList<Box<Integer>> listOfIntegerBoxes = new 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);

2、泛型类的类型推断和实例化

只要编译器可以从上下文推断类型参数,您就可以用一组空的类型参数 (<>) 替换调用泛型类的构造函数所需的类型参数。 这对尖括号非正式地称为菱形。例如,考虑以下变量声明:

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

3、泛型和非泛型类的类型推断和泛型构造函数

请注意,构造函数在泛型和非泛型类中都可以是泛型的(换句话说,声明它们自己的形式类型参数)。 考虑以下示例:

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。

[注意]  推理算法仅使用调用参数、目标类型和可能的明显预期返回类型来推断类型。 推理算法不使用程序后面的结果。

4、目标类型

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 表达式中的 目标类型

 

posted @ 2021-09-16 16:17  meow_world  阅读(1335)  评论(0)    收藏  举报