Java™ 教程(型別推斷)

博弈發表於2019-01-19

型別推斷

型別推斷是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>的例項,該語句顯式指定泛型類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表示式中的目標型別


上一篇:泛型、繼承和子型別

下一篇:泛型萬用字元

相關文章