本篇博客主要介紹了Java類型擦除的定義,詳細的介紹了類型擦除在Java中所出現的場景。html
爲了讓大家快速的對類型擦除有一個印象,首先舉一個很簡單也很經典的例子。java
// 指定泛型爲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被擦除掉了。這就是類型擦除的一個典型的例子。node
而若是咱們說到類型擦除爲何會出現,咱們就必需要了解泛型。web
隨着2004年9月30日,工程代號爲Tiger的JDK 1.5發佈,泛型今後與你們見面。JDK 1.5在Java語法的易用性上做出了很是大的改進。除了泛型,同版本加入的還有自動裝箱、動態註解、枚舉、可變長參數、foreach循環等等。安全
而在1.5以前的版本中,爲了讓Java的類具備通用性,參數類型和返回類型一般都設置爲Object,可見,若是須要不用的類型,就須要在相應的地方,對其進行強制轉換,程序才能夠正常運行,十分麻煩,稍不注意就會出錯。服務器
泛型的本質就是參數化類型。也就是,將一個數據類型指定爲參數。引入泛型有什麼好處呢?微信
泛型能夠將JDK 1.5以前在運行時才能發現的錯誤,提早到編譯期。也就是說,泛型提供了編譯時類型安全的檢測機制。例如,一個變量原本是Integer類型,咱們在代碼中設置成了String,沒有使用泛型的時候只有在代碼運行到這了,纔會報錯。函數
而引入泛型以後就不會出現這個問題。這是由於經過泛型能夠知道該參數的規定類型,而後在編譯時,判斷其類型是否符合規定類型。工具
泛型總共有三種使用方法,分別使用於類、方法和接口。網站
簡單的泛型類能夠定義爲以下。
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,這只是你們約定俗成的習慣而已。有了上述的泛型類以後咱們就能夠像以下的方式使用了。
// 假設有這樣一個具體的類 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
。
首先咱們看一下不帶返回值的泛型方法,能夠定義爲以下結構。
// 定義不帶返回值的泛型方法 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; }
// 調用不帶返回值泛型方法 genericMethod("This is string"); // class java.lang.String genericMethod(56L); // class java.lang.Long // 調用帶返回值的泛型方法 String test = genericWithReturnMethod("TEST"); // TEST class java.lang.String
帶返回值的方法中,T就是當前函數的返回類型。
泛型接口定義以下
public interface genericInterface<T> { }
使用的方法與泛型類相似,這裏就再也不贅述。
什麼是泛型通配符?官方一點的解釋是
Type of unknown.
也就是無限定的通配符,能夠表明任意類型。用法也有三種,<?>,<? extends T>和<? super T>。
既然已經有了T這樣的表明任意類型的通配符,爲何還須要這樣一個無限定的通配符呢?是由於其主要解決的問題是泛型繼承帶來的問題。
首先來看一個例子
List<Integer> integerList = new ArrayList<>(); List<Number> numberList = integerList;
咱們知道,Integer
是繼承自Number
類的。
public final class Integer extends Number implements Comparable
{
....
}
那麼上述的代碼可以經過編譯嗎?確定是不行的。Integer繼承自Number不表明List
在其餘函數中,例如JavaScript中,一個函數的參數能夠是任意的類型,而不須要進行任意的類型轉換,因此這樣的函數在某些應用場景下,就會具備很強的通用性。
而在Java這種強類型語言中,一個函數的參數類型是固定不變的。那若是想要在Java中實現相似於JavaScript那樣的通用函數該怎麼辦呢?這也就是爲何咱們須要泛型的通配符。
假設咱們有不少動物的類, 例如Dog, Pig和Cat三個類,咱們須要有一個通用的函數來計算動物列表中的全部動物的腿的總數,若是在Java中,要怎麼作呢?
可能會有人說,用泛型啊,泛型不就是解決這個問題的嗎?泛型必須指定一個特定的類型。正式由於泛型解決不了...才提出了泛型的通配符。
無界通配符就是?
。看到這你可能會問,這不是跟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,上面咱們也列舉了如何定義泛型的方法以及若是調用泛型方法。泛型方法內部是要去關心具體類型的,而不只僅是數量和不爲空這麼簡單。
既然?
能夠表明任何類型,那麼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);
看完了例子,咱們就能夠簡單的得出一個結論。上界通配符就是一個能夠處理任何特定類型以及是該特定類型的派生類的通配符。
可能會有人看的有點懵逼,我結合上面的例子,再簡單的用人話解釋一下:上界通配符就是一個啥動物都能放的盒子。
上面咱們聊了上界通配符,它將未知的類型限制爲特定類型或者該特定的類型的子類型(也就是上面討論過的動物以及一切動物的子類)。而下界通配符則將未知的類型限制爲特定類型或者該特定的類型的超類型,也就是超類或者基類。
在上述的上界通配符中,咱們舉了一個例子。寫了一個能夠處理任何動物類以及是動物類的派生類的函數。而如今咱們要寫一個函數,用來處理任何是Integer以及是Integer的超類的函數。
public static void addNumbers(List<? super Integer> list) { for (int i = 1; i <= 10; i++) { list.add(i); } }
簡單的瞭解了泛型的幾種簡單的使用方法以後,咱們回到本篇博客的主題上來——類型擦除。泛型雖然有上述所列出的一些好處,可是泛型的生命週期只限於編譯階段。
本文最開始的給出的樣例就是一個典型的例子。在通過編譯以後會採起去泛型化的措施,編譯的過程當中,在檢測了泛型的結果以後會將泛型的相關信息進行擦除操做。就像文章最開始提到的例子同樣,咱們使用上面定義好的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。相應的,泛型類中定義的方法的類型也是如此。
若是各位發現文章中有問題的,歡迎你們不吝賜教,我會及時的更正。
參考:
往期文章:
相關:
- 我的網站: Lunhao Hu
- 微信公衆號: SH的全棧筆記(或直接在添加公衆號界面搜索微信號LunhaoHu)