本篇部落客要介紹了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 和 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. 寫在最後
如果各位發現文章中有問題的,歡迎大家不吝賜教,我會及時的更正。
參考: 往期文章: 相關:
- 個人網站: Lunhao Hu