初探Java型別擦除

detectiveHLH發表於2019-05-27

本篇部落格主要介紹了Java型別擦除的定義,詳細的介紹了型別擦除在Java中所出現的場景。

1. 什麼是型別擦除

為了讓你們快速的對型別擦除有一個印象,首先舉一個很簡單也很經典的例子。

// 指定泛型為String
List<String> list1 = new ArrayList<>();
// 指定泛型為Integer
List<Integer> list2 = new ArrayList<>();

System.out.println(list1.getClass() == list2.getClass()); // true

上面的判斷結果是true。代表了兩個傳入了不同泛型的List最終都編譯成了ArrayList,成為了同一種型別,原來的泛型引數String和Integer被擦除掉了。這就是型別擦除的一個典型的例子。

而如果我們說到型別擦除為什麼會出現,我們就必須要了解泛型。

2. 泛型

2.1. 泛型的定義

隨著2004年9月30日,工程代號為Tiger的JDK 1.5釋出,泛型從此與大家見面。JDK 1.5在Java語法的易用性上作出了非常大的改進。除了泛型,同版本加入的還有自動裝箱、動態註解、列舉、可變長引數、foreach迴圈等等。

而在1.5之前的版本中,為了讓Java的類具有通用性,引數型別和返回型別通常都設定為Object,可見,如果需要不用的型別,就需要在相應的地方,對其進行強制轉換,程式才可以正常執行,十分麻煩,稍不注意就會出錯。

泛型的本質就是引數化型別。也就是,將一個資料型別指定為引數。引入泛型有什麼好處呢?

泛型可以將JDK 1.5之前在執行時才能發現的錯誤,提前到編譯期。也就是說,泛型提供了編譯時型別安全的檢測機制。例如,一個變數本來是Integer型別,我們在程式碼中設定成了String,沒有使用泛型的時候只有在程式碼執行到這了,才會報錯。

而引入泛型之後就不會出現這個問題。這是因為通過泛型可以知道該引數的規定型別,然後在編譯時,判斷其型別是否符合規定型別。

泛型總共有三種使用方法,分別使用於類、方法和介面。

3. 泛型的使用方法

3.1 泛型類

3.1.1 定義泛型類

簡單的泛型類可以定義為如下。

public class Generic<T> {
    T data;
    
    public Generic(T data) {
        setData(data);
    }
    
    public T getData() {
        return data;
    }
    
    public void setData(T data) {
        this.data = data;
    }
}

其中的T代表引數型別,代表任何型別。當然,並不是一定要寫成T,這只是大家約定俗成的習慣而已。有了上述的泛型類之後我們就可以像如下的方式使用了。

3.1.2 使用泛型類

// 假設有這樣一個具體的類
public class Hello {
    private Integer id;

    private String name;

    private Integer age;

    private String email;
}

// 使用泛型類
Hello hello = new Hello();
Generic<Hello> result = new Generic<>();
resule.setData(hello);

// 通過泛型類獲取資料
Hello data = result.getData();

當然如果泛型類不傳入指定的型別的話,泛型類中的方法或者成員變數定義的型別可以為任意型別,如果列印result.getClass()的話,會得到Generic

3.2. 泛型方法

3.2.1 定義泛型方法

首先我們看一下不帶返回值的泛型方法,可以定義為如下結構。

// 定義不帶返回值的泛型方法
public <T> void genericMethod(T field) {
    System.out.println(field.getClass().toString());
}

// 定義帶返回值的泛型方法
private <T> T genericWithReturnMethod(T field) {
    System.out.println(field.getClass().toString());
    return field;
}

3.2.2 呼叫泛型方法

// 呼叫不帶返回值泛型方法
genericMethod("This is string"); // class java.lang.String
genericMethod(56L); // class java.lang.Long

// 呼叫帶返回值的泛型方法
String test = genericWithReturnMethod("TEST"); // TEST class java.lang.String

帶返回值的方法中,T就是當前函式的返回型別。

3.3. 泛型介面

泛型介面定義如下

public interface genericInterface<T> {
}

使用的方法與泛型類類似,這裡就不再贅述。

4. 泛型萬用字元

什麼是泛型萬用字元?官方一點的解釋是

Type of unknown.

也就是無限定的萬用字元,可以代表任意型別。用法也有三種,<?>,<? extends T>和<? super T>。

既然已經有了T這樣的代表任意型別的萬用字元,為什麼還需要這樣一個無限定的萬用字元呢?是因為其主要解決的問題是泛型繼承帶來的問題。

4.1. 泛型的繼承問題

首先來看一個例子

List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerList;

我們知道,Integer是繼承自Number類的。

public final class Integer extends Number implements Comparable

那麼上述的程式碼能夠通過編譯嗎?肯定是不行的。Integer繼承自Number不代表List

4.2. 萬用字元的應用場景

在其他函式中,例如JavaScript中,一個函式的引數可以是任意的型別,而不需要進行任意的型別轉換,所以這樣的函式在某些應用場景下,就會具有很強的通用性。

而在Java這種強型別語言中,一個函式的引數型別是固定不變的。那如果想要在Java中實現類似於JavaScript那樣的通用函式該怎麼辦呢?這也就是為什麼我們需要泛型的萬用字元。

假設我們有很多動物的類, 例如Dog, Pig和Cat三個類,我們需要有一個通用的函式來計算動物列表中的所有動物的腿的總數,如果在Java中,要怎麼做呢?

可能會有人說,用泛型啊,泛型不就是解決這個問題的嗎?泛型必須指定一個特定的型別。正式因為泛型解決不了...才提出了泛型的萬用字元。

4.3. 無界萬用字元

無界萬用字元就是?。看到這你可能會問,這不是跟T一樣嗎?為啥還要搞個?。他們主要區別在於,T主要用於宣告一個泛型類或者方法,?主要用於使用泛型類和泛型方法。下面舉個簡單的例子。

// 定義列印任何型別列表的函式
public static void printList(List<?> list) {
    for (Object elem: list) {
        System.out.print(elem + " ");
    }
}

// 呼叫上述函式
List<Integer> intList = Arrays.asList(1, 2, 3);
List<String> stringList = Arrays.asList("one", "two", "three");
printList(li);// 1 2 3 
printList(ls);// one two three

上述函式的目的是列印任何型別的列表。可以看到在函式內部,並沒有關心List中的泛型到底是什麼型別的,你可以將<?>理解為只提供了一個只讀的功能,它去除了增加具體元素的能力,只保留與具體型別無關的功能。從上述的例子可以看出,它只關心元素的數量以及其是否為空,除此之外不關心任何事。

再反觀T,上面我們也列舉了如何定義泛型的方法以及如果呼叫泛型方法。泛型方法內部是要去關心具體型別的,而不僅僅是數量和不為空這麼簡單。

4.4. 上界萬用字元<? extends T>

既然?可以代表任何型別,那麼extends又是幹嘛的呢?

假設有這樣一個需求,我們只允許某一些特定的型別可以呼叫我們的函式(例如,所有的Animal類以及其派生類),但是目前使用?,所有的型別都可以呼叫函式,無法滿足我們的需求。

private int countLength(List< ? extends Animal> list) {...}

使用了上界萬用字元來完成這個公共函式之後,就可以使用如下的方式來呼叫它了。

List<Pig> pigs = new ArrayList<>();
List<Dog> dogs = new ArrayList<>();
List<Cat> cats = new ArrayList<>();

// 假裝寫入了資料
int sum = 0;
sum += countLength(pigs);
sum += countLength(dogs);
sum += countLength(cats);

看完了例子,我們就可以簡單的得出一個結論。上界萬用字元就是一個可以處理任何特定型別以及是該特定型別的派生類的萬用字元。

可能會有人看的有點懵逼,我結合上面的例子,再簡單的用人話解釋一下:上界萬用字元就是一個啥動物都能放的盒子。

4.5. 下界萬用字元<? super Animal>

上面我們聊了上界萬用字元,它將未知的型別限制為特定型別或者該特定的型別的子型別(也就是上面討論過的動物以及一切動物的子類)。而下界萬用字元則將未知的型別限制為特定型別或者該特定的型別的超型別,也就是超類或者基類。

在上述的上界萬用字元中,我們舉了一個例子。寫了一個可以處理任何動物類以及是動物類的派生類的函式。而現在我們要寫一個函式,用來處理任何是Integer以及是Integer的超類的函式。

public static void addNumbers(List<? super Integer> list) {
    for (int i = 1; i <= 10; i++) {
        list.add(i);
    }
}

5. 型別擦除

簡單的瞭解了泛型的幾種簡單的使用方法之後,我們回到本篇部落格的主題上來——型別擦除。泛型雖然有上述所列出的一些好處,但是泛型的生命週期只限於編譯階段。

本文最開始的給出的樣例就是一個典型的例子。在經過編譯之後會採取去泛型化的措施,編譯的過程中,在檢測了泛型的結果之後會將泛型的相關資訊進行擦除操作。就像文章最開始提到的例子一樣,我們使用上面定義好的Generic泛型類來舉個簡單的例子。

Generic<String> generic = new Generic<>("Hello");
Field[] fs = generic.getClass().getDeclaredFields();
for (Field f : fs) {
    System.out.println("type: " + f.getType().getName()); // type: java.lang.Object
}

getDeclaredFields是反射中的方法,可以獲取當前類已經宣告的各種欄位,包括public,protected以及private。

可以看到我們傳入的泛型String已經被擦除了,取而代之的是Object。那之前的String和Integer的泛型資訊去哪兒了呢?可能這個時候你會靈光一閃,那是不是所有的泛型在被擦除之後都會變成Object呢?彆著急,繼續往下看。

當我們在泛型上面使用了上界萬用字元以後,會有什麼情況發生呢?我們將Generic類改成如下形式。

public class Generic<T extends String> {
    T data;

    public Generic(T data) {
        setData(data);
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

然後再次使用反射來檢視泛型擦除之後型別。這次控制檯會輸出type: java.lang.String。可以看到,如果我們給泛型類制定了上限,泛型擦除之後就會被替換成型別的上限。而如果沒有指定,就會統一的被替換成Object。相應的,泛型類中定義的方法的型別也是如此。

6. 寫在最後

如果各位發現文章中有問題的,歡迎大家不吝賜教,我會及時的更正。

參考:

  1. Java語言型別擦除
  2. 下界萬用字元
  3. List<?>和List

相關文章