本篇博客主要介紹了Java類型擦除的定義,詳細的介紹了類型擦除在Java中所出現的場景惦界。
1. 什么是類型擦除
為了讓你們快速的對類型擦除有一個印象衬潦,首先舉一個很簡單也很經典的例子蜡励。
// 指定泛型為StringList list1 =newArrayList<>();// 指定泛型為IntegerList list2 =newArrayList<>();System.out.println(list1.getClass() == list2.getClass());// true
上面的判斷結果是?true?。代表了兩個傳入了不同泛型的List最終都編譯成了ArrayList锦担,成為了同一種類型俭识,原來的泛型參數String和Integer被擦除掉了。這就是類型擦除的一個典型的例子洞渔。
而如果我們說到類型擦除為什么會出現套媚,我們就必須要了解泛型。
2. 泛型
2.1. 泛型的定義
隨著2004年9月30日磁椒,工程代號為Tiger的JDK 1.5發(fā)布堤瘤,泛型從此與大家見面。JDK 1.5在Java語法的易用性上作出了非常大的改進浆熔。除了泛型本辐,同版本加入的還有自動裝箱、動態(tài)注解、枚舉慎皱、可變長參數老虫、foreach循環(huán)等等。
而在1.5之前的版本中茫多,為了讓Java的類具有通用性祈匙,參數類型和返回類型通常都設置為Object,可見天揖,如果需要不用的類型夺欲,就需要在相應的地方,對其進行強制轉換今膊,程序才可以正常運行些阅,十分麻煩,稍不注意就會出錯万细。
泛型的本質就是參數化類型。也就是纸泄,將一個數據類型指定為參數赖钞。引入泛型有什么好處呢?
泛型可以將JDK 1.5之前在運行時才能發(fā)現的錯誤聘裁,提前到編譯期雪营。也就是說,泛型提供了編譯時類型安全的檢測機制衡便。例如献起,一個變量本來是Integer類型,我們在代碼中設置成了String镣陕,沒有使用泛型的時候只有在代碼運行到這了谴餐,才會報錯。
而引入泛型之后就不會出現這個問題呆抑。這是因為通過泛型可以知道該參數的規(guī)定類型岂嗓,然后在編譯時,判斷其類型是否符合規(guī)定類型鹊碍。
泛型總共有三種使用方法厌殉,分別使用于類、方法和接口侈咕。
3. 泛型的使用方法
3.1 泛型類
3.1.1 定義泛型類
簡單的泛型類可以定義為如下公罕。
publicclassGeneric{? ? Tdata;publicGeneric(Tdata) {? ? ? ? setData(data);? ? }publicT getData() {returndata;? ? }publicvoid setData(Tdata) {this.data=data;? ? }}
其中的T代表參數類型,代表任何類型耀销。當然楼眷,并不是一定要寫成T,這只是大家約定俗成的習慣而已。有了上述的泛型類之后我們就可以像如下的方式使用了摩桶。
3.1.2 使用泛型類
// 假設有這樣一個具體的類publicclassHello {privateInteger id;privateStringname;privateInteger age;privateStringemail;}// 使用泛型類Hello hello =newHello();Generic result =newGeneric<>();resule.setData(hello);// 通過泛型類獲取數據Hello data = result.getData();
當然如果泛型類不傳入指定的類型的話桥状,泛型類中的方法或者成員變量定義的類型可以為任意類型,如果打印?result.getClass()?的話硝清,會得到?Generic?辅斟。
3.2. 泛型方法
3.2.1 定義泛型方法
首先我們看一下不帶返回值的泛型方法,可以定義為如下結構芦拿。
// 定義不帶返回值的泛型方法publicvoidgenericMethod(T field){? ? System.out.println(field.getClass().toString());}// 定義帶返回值的泛型方法privateTgenericWithReturnMethod(T field){? ? System.out.println(field.getClass().toString());returnfield;}
3.2.2 調用泛型方法
// 調用不帶返回值泛型方法genericMethod("This is string");// class java.lang.StringgenericMethod(56L);// class java.lang.Long// 調用帶返回值的泛型方法Stringtest = genericWithReturnMethod("TEST");// TEST class java.lang.String
帶返回值的方法中士飒,T就是當前函數的返回類型。
3.3. 泛型接口
泛型接口定義如下
publicinterfacegenericInterface{}
使用的方法與泛型類類似蔗崎,這里就不再贅述酵幕。
4. 泛型通配符
什么是泛型通配符?官方一點的解釋是
Typeofunknown.
也就是無限定的通配符缓苛,可以代表任意類型芳撒。用法也有三種,<?>未桥,<? extends T>和<? super T>笔刹。
既然已經有了T這樣的代表任意類型的通配符,為什么還需要這樣一個無限定的通配符呢冬耿?是因為其主要解決的問題是泛型繼承帶來的問題舌菜。
4.1. 泛型的繼承問題
首先來看一個例子
ListintegerList = new ArrayList<>();ListnumberList = integerList;
我們知道,?Integer?是繼承自?Number?類的亦镶。
publicfinalclassIntegerextendsNumberimplementsComparable
那么上述的代碼能夠通過編譯嗎日月?肯定是不行的。Integer繼承自Number不代表List
4.2. 通配符的應用場景
在其他函數中缤骨,例如JavaScript中爱咬,一個函數的參數可以是任意的類型,而不需要進行任意的類型轉換绊起,所以這樣的函數在某些應用場景下台颠,就會具有很強的通用性。
而在Java這種強類型語言中勒庄,一個函數的參數類型是固定不變的串前。那如果想要在Java中實現類似于JavaScript那樣的通用函數該怎么辦呢?這也就是為什么我們需要泛型的通配符实蔽。
假設我們有很多動物的類, 例如Dog, Pig和Cat三個類荡碾,我們需要有一個通用的函數來計算動物列表中的所有動物的腿的總數,如果在Java中局装,要怎么做呢坛吁?
可能會有人說劳殖,用泛型啊,泛型不就是解決這個問題的嗎拨脉?泛型必須指定一個特定的類型哆姻。正式因為泛型解決不了...才提出了泛型的通配符。
4.3. 無界通配符
無界通配符就是???玫膀∶В看到這你可能會問,這不是跟T一樣嗎帖旨?為啥還要搞個???箕昭。他們主要區(qū)別在于,T主要用于聲明一個泛型類或者方法解阅,?主要用于使用泛型類和泛型方法落竹。下面舉個簡單的例子。
// 定義打印任何類型列表的函數publicstaticvoid printList(List<?>list) {for(Object elem:list) {? ? ? ? System.out.print(elem +" ");? ? }}// 調用上述函數List intList = Arrays.asList(1,2,3);List stringList = Arrays.asList("one","two","three");printList(li);// 1 2 3 printList(ls);// one two three
上述函數的目的是打印任何類型的列表货抄∈稣伲可以看到在函數內部,并沒有關心List中的泛型到底是什么類型的蟹地,你可以將<?>理解為只提供了一個只讀的功能积暖,它去除了增加具體元素的能力,只保留與具體類型無關的功能锈津。從上述的例子可以看出呀酸,它只關心元素的數量以及其是否為空凉蜂,除此之外不關心任何事琼梆。
再反觀T偎行,上面我們也列舉了如何定義泛型的方法以及如果調用泛型方法涂籽。泛型方法內部是要去關心具體類型的椭坚,而不僅僅是數量和不為空這么簡單桩引。
4.4. 上界通配符<? extends T>
既然???可以代表任何類型吮播,那么extends又是干嘛的呢纲辽?
假設有這樣一個需求祈纯,我們只允許某一些特定的類型可以調用我們的函數(例如糠爬,所有的Animal類以及其派生類)轧邪,但是目前使用???刽脖,所有的類型都可以調用函數,無法滿足我們的需求忌愚。
privateintcountLength(List< ? extends Animal>list){...}
使用了上界通配符來完成這個公共函數之后曲管,就可以使用如下的方式來調用它了。
List pigs =newArrayList<>();List dogs =newArrayList<>();List cats =newArrayList<>();// 假裝寫入了數據intsum =0;sum += countLength(pigs);sum += countLength(dogs);sum += countLength(cats);
看完了例子硕糊,我們就可以簡單的得出一個結論院水。上界通配符就是一個可以處理任何特定類型以及是該特定類型的派生類的通配符腊徙。
可能會有人看的有點懵逼,我結合上面的例子檬某,再簡單的用人話解釋一下:上界通配符就是一個啥動物都能放的盒子撬腾。
4.5. 下界通配符<? super Animal>
上面我們聊了上界通配符,它將未知的類型限制為特定類型或者該特定的類型的子類型(也就是上面討論過的動物以及一切動物的子類)恢恼。而下界通配符則將未知的類型限制為特定類型或者該特定的類型的超類型民傻,也就是超類或者基類。
在上述的上界通配符中厅瞎,我們舉了一個例子饰潜。寫了一個可以處理任何動物類以及是動物類的派生類的函數。而現在我們要寫一個函數和簸,用來處理任何是Integer以及是Integer的超類的函數彭雾。
publicstaticvoidaddNumbers(Listlist){for(inti =1; i <=10; i++) {list.add(i);? ? }}
5. 類型擦除
簡單的了解了泛型的幾種簡單的使用方法之后,我們回到本篇博客的主題上來——類型擦除锁保。泛型雖然有上述所列出的一些好處薯酝,但是泛型的生命周期只限于編譯階段。
本文最開始的給出的樣例就是一個典型的例子爽柒。在經過編譯之后會采取去泛型化的措施吴菠,編譯的過程中,在檢測了泛型的結果之后會將泛型的相關信息進行擦除操作浩村。就像文章最開始提到的例子一樣做葵,我們使用上面定義好的Generic泛型類來舉個簡單的例子。
Generic generic =newGeneric<>("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呢?別著急隐绵,繼續(xù)往下看之众。
當我們在泛型上面使用了上界通配符以后,會有什么情況發(fā)生呢依许?我們將Generic類改成如下形式棺禾。
publicclassGeneric{? ? Tdata;publicGeneric(Tdata) {? ? ? ? setData(data);? ? }publicT getData() {returndata;? ? }publicvoid setData(Tdata) {this.data=data;? ? }}
然后再次使用反射來查看泛型擦除之后類型。這次控制臺會輸出?type: java.lang.String?悍手×蹦溃可以看到袍患,如果我們給泛型類制定了上限,泛型擦除之后就會被替換成類型的上限竣付。而如果沒有指定诡延,就會統(tǒng)一的被替換成Object。相應的古胆,泛型類中定義的方法的類型也是如此肆良。
我自己是一個從事了6年的Java全棧工程師,最近整理了一套適合2019年學習的Java\大數據資料逸绎,從基礎的Java惹恃、大數據面向對象到進階的框架知識都有整理哦,可以來我的主頁免費領取哦棺牧。