java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题
原
java泛型(二)、泛型的內(nèi)部原理:類型擦除以及類型擦除帶來的問題
2012年08月29日 23:44:10?Kilnn?閱讀數(shù):56717
?版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。 https://blog.csdn.net/LonelyRoamer/article/details/7868820
參考:java核心技術(shù)
一、Java泛型的實(shí)現(xiàn)方法:類型擦除
前面已經(jīng)說了,Java的泛型是偽泛型。為什么說Java的泛型是偽泛型呢?因?yàn)?#xff0c;在編譯期間,所有的泛型信息都會被擦除掉。正確理解泛型概念的首要前提是理解類型擦出(type erasure)。
Java中的泛型基本上都是在編譯器這個(gè)層次來實(shí)現(xiàn)的。在生成的Java字節(jié)碼中是不包含泛型中的類型信息的。使用泛型的時(shí)候加上的類型參數(shù),會在編譯器在編譯的時(shí)候去掉。這個(gè)過程就稱為類型擦除。
如在代碼中定義的List<object>和List<String>等類型,在編譯后都會編程List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。Java編譯器會在編譯時(shí)盡可能的發(fā)現(xiàn)可能出錯的地方,但是仍然無法避免在運(yùn)行時(shí)刻出現(xiàn)類型轉(zhuǎn)換異常的情況。類型擦除也是Java的泛型實(shí)現(xiàn)方法與C++模版機(jī)制實(shí)現(xiàn)方式之間的重要區(qū)別。
可以通過兩個(gè)簡單的例子,來證明java泛型的類型擦除。
例1、
?
public class Test4 {
public static void main(String[] args) {
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add("abc");
ArrayList<Integer> arrayList2=new ArrayList<Integer>();
arrayList2.add(123);
System.out.println(arrayList1.getClass()==arrayList2.getClass());
}
}
在這個(gè)例子中,我們定義了兩個(gè)ArrayList數(shù)組,不過一個(gè)是ArrayList<String>泛型類型,只能存儲字符串。一個(gè)是ArrayList<Integer>泛型類型,只能存儲整形。最后,我們通過arrayList1對象和arrayList2對象的getClass方法獲取它們的類的信息,最后發(fā)現(xiàn)結(jié)果為true。說明泛型類型String和Integer都被擦除掉了,只剩下了原始類型。
?
例2、
?
public class Test4 {
public static void main(String[] args) throws IllegalArgumentException, SecurityException, IllegalAccessException, InvocationTargetException, NoSuchMethodException {
ArrayList<Integer> arrayList3=new ArrayList<Integer>();
arrayList3.add(1);//這樣調(diào)用add方法只能存儲整形,因?yàn)榉盒皖愋偷膶?shí)例為Integer
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i=0;i<arrayList3.size();i++) {
System.out.println(arrayList3.get(i));
}
}
在程序中定義了一個(gè)ArrayList泛型類型實(shí)例化為Integer的對象,如果直接調(diào)用add方法,那么只能存儲整形的數(shù)據(jù)。不過當(dāng)我們利用反射調(diào)用add方法的時(shí)候,卻可以存儲字符串。這說明了Integer泛型實(shí)例在編譯之后被擦除了,只保留了原始類型。
?
?
?
二、類型擦除后保留的原始類型
在上面,兩次提到了原始類型,什么是原始類型?原始類型(raw type)就是擦除去了泛型信息,最后在字節(jié)碼中的類型變量的真正類型。無論何時(shí)定義一個(gè)泛型類型,相應(yīng)的原始類型都會被自動地提供。類型變量被擦除(crased),并使用其限定類型(無限定的變量用Object)替換。
?
例3:
?
class Pair<T> {
?? ?private T value;
?? ?public T getValue() {
?? ??? ?return value;
?? ?}
?? ?public void setValue(T? value) {
?? ??? ?this.value = value;
?? ?}
}
Pair<T>的原始類型為:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
因?yàn)樵赑air<T>中,T是一個(gè)無限定的類型變量,所以用Object替換。其結(jié)果就是一個(gè)普通的類,如同泛型加入java變成語言之前已經(jīng)實(shí)現(xiàn)的那樣。在程序中可以包含不同類型的Pair,如Pair<String>或Pair<Integer>,但是,擦除類型后它們就成為原始的Pair類型了,原始類型都是Object。
從上面的那個(gè)例2中,我們也可以明白ArrayList<Integer>被擦除類型后,原始類型也變成了Object,所以通過反射我們就可以存儲字符串了。
?
如果類型變量有限定,那么原始類型就用第一個(gè)邊界的類型變量來替換。
比如Pair這樣聲明
例4:
?
public class Pair<T extends Comparable& Serializable> {那么原始類型就是Comparable
?
注意:
如果Pair這樣聲明public class Pair<T extends Serializable&Comparable> ,那么原始類型就用Serializable替換,而編譯器在必要的時(shí)要向Comparable插入強(qiáng)制類型轉(zhuǎn)換。為了提高效率,應(yīng)該將標(biāo)簽(tagging)接口(即沒有方法的接口)放在邊界限定列表的末尾。
?
?
要區(qū)分原始類型和泛型變量的類型
在調(diào)用泛型方法的時(shí)候,可以指定泛型,也可以不指定泛型。
在不指定泛型的情況下,泛型變量的類型為 該方法中的幾種類型的同一個(gè)父類的最小級,直到Object。
在指定泛型的時(shí)候,該方法中的幾種類型必須是該泛型實(shí)例類型或者其子類。
public class Test2{
public static void main(String[] args) {
/**不指定泛型的時(shí)候*/
int i=Test2.add(1, 2); //這兩個(gè)參數(shù)都是Integer,所以T為Integer類型
Number f=Test2.add(1, 1.2);//這兩個(gè)參數(shù)一個(gè)是Integer,以風(fēng)格是Float,所以取同一父類的最小級,為Number
Object o=Test2.add(1, "asd");//這兩個(gè)參數(shù)一個(gè)是Integer,以風(fēng)格是Float,所以取同一父類的最小級,為Object
?/**指定泛型的時(shí)候*/
int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能為Integer類型或者其子類
int b=Test2.<Integer>add(1, 2.2);//編譯錯誤,指定了Integer,不能為Float
Number c=Test2.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}
//這是一個(gè)簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
其實(shí)在泛型類中,不指定泛型的時(shí)候,也差不多,只不過這個(gè)時(shí)候的泛型類型為Object,就比如ArrayList中,如果不指定泛型,那么這個(gè)ArrayList中可以放任意類型的對象。
舉例:
public static void main(String[] args) {
ArrayList arrayList=new ArrayList();
arrayList.add(1);
arrayList.add("121");
arrayList.add(new Date());
}
?
三、類型擦除引起的問題及解決方法
因?yàn)榉N種原因,Java不能實(shí)現(xiàn)真正的泛型,只能使用類型擦除來實(shí)現(xiàn)偽泛型,這樣雖然不會有類型膨脹的問題,但是也引起了許多新的問題。所以,Sun對這些問題作出了許多限制,避免我們犯各種錯誤。
?
1、先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題
既然說類型變量會在編譯的時(shí)候擦除掉,那為什么我們往ArrayList<String> arrayList=new ArrayList<String>();所創(chuàng)建的數(shù)組列表arrayList中,不能使用add方法添加整形呢?不是說泛型變量Integer會在編譯時(shí)候擦除變?yōu)樵碱愋蚈bject嗎,為什么不能存別的類型呢?既然類型擦除了,如何保證我們只能使用泛型變量限定的類型呢?
java是如何解決這個(gè)問題的呢?java編譯器是通過先檢查代碼中泛型的類型,然后再進(jìn)行類型擦除,在進(jìn)行編譯的。
舉個(gè)例子說明:
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("123");
arrayList.add(123);//編譯錯誤
}
在上面的程序中,使用add方法添加一個(gè)整形,在eclipse中,直接就會報(bào)錯,說明這就是在編譯之前的檢查。因?yàn)槿绻窃诰幾g之后檢查,類型擦除后,原始類型為Object,是應(yīng)該運(yùn)行任意引用類型的添加的。可實(shí)際上卻不是這樣,這恰恰說明了關(guān)于泛型變量的使用,是會在編譯之前檢查的。
?
那么,這么類型檢查是針對誰的呢?我們先看看參數(shù)化類型與原始類型的兼容
以ArrayList舉例子,以前的寫法:
?
ArrayList arrayList=new ArrayList();現(xiàn)在的寫法:
ArrayList<String> arrayList=new ArrayList<String>();
如果是與以前的代碼兼容,各種引用傳值之間,必然會出現(xiàn)如下的情況:
這樣是沒有錯誤的,不過會有個(gè)編譯時(shí)警告。
?
不過在第一種情況,可以實(shí)現(xiàn)與 完全使用泛型參數(shù)一樣的效果,第二種則完全沒效果。
因?yàn)?#xff0c;本來類型檢查就是編譯時(shí)完成的。new ArrayList()只是在內(nèi)存中開辟一個(gè)存儲空間,可以存儲任何的類型對象。而真正涉及類型檢查的是它的引用,因?yàn)槲覀兪鞘褂盟胊rrayList1 來調(diào)用它的方法,比如說調(diào)用add()方法。所以arrayList1引用能完成泛型類型的檢查。
而引用arrayList2沒有使用泛型,所以不行。
舉例子:
public class Test10 {
public static void main(String[] args) {
//
ArrayList<String> arrayList1=new ArrayList();
arrayList1.add("1");//編譯通過
arrayList1.add(1);//編譯錯誤
String str1=arrayList1.get(0);//返回類型就是String
ArrayList arrayList2=new ArrayList<String>();
arrayList2.add("1");//編譯通過
arrayList2.add(1);//編譯通過
Object object=arrayList2.get(0);//返回類型就是Object
new ArrayList<String>().add("11");//編譯通過
new ArrayList<String>().add(22);//編譯錯誤
String string=new ArrayList<String>().get(0);//返回類型就是String
}
}
通過上面的例子,我們可以明白,類型檢查就是針對引用的,誰是一個(gè)引用,用這個(gè)引用調(diào)用泛型方法,就會對這個(gè)引用調(diào)用的方法進(jìn)行類型檢測,而無關(guān)它真正引用的對象。
?
?
從這里,我們可以再討論下 泛型中參數(shù)化類型為什么不考慮繼承關(guān)系
在Java中,像下面形式的引用傳遞是不允許的:
ArrayList<String> arrayList1=new ArrayList<Object>();//編譯錯誤
ArrayList<Object> arrayList1=new ArrayList<String>();//編譯錯誤
我們先看第一種情況,將第一種情況拓展成下面的形式:
?
ArrayList<Object> arrayList1=new ArrayList<Object>();
arrayList1.add(new Object());
arrayList1.add(new Object());
ArrayList<String> arrayList2=arrayList1;//編譯錯誤
實(shí)際上,在第4行代碼的時(shí)候,就會有編譯錯誤。那么,我們先假設(shè)它編譯沒錯。那么當(dāng)我們使用arrayList2引用用get()方法取值的時(shí)候,返回的都是String類型的對象(上面提到了,類型檢測是根據(jù)引用來決定的。),可是它里面實(shí)際上已經(jīng)被我們存放了Object類型的對象,這樣,就會有ClassCastException了。所以為了避免這種極易出現(xiàn)的錯誤,Java不允許進(jìn)行這樣的引用傳遞。(這也是泛型出現(xiàn)的原因,就是為了解決類型轉(zhuǎn)換的問題,我們不能違背它的初衷)。
?
?
在看第二種情況,將第二種情況拓展成下面的形式:
ArrayList<String> arrayList1=new ArrayList<String>();
arrayList1.add(new String());
arrayList1.add(new String());
ArrayList<Object> arrayList2=arrayList1;//編譯錯誤
沒錯,這樣的情況比第一種情況好的多,最起碼,在我們用arrayList2取值的時(shí)候不會出現(xiàn)ClassCastException,因?yàn)槭菑腟tring轉(zhuǎn)換為Object。可是,這樣做有什么意義呢,泛型出現(xiàn)的原因,就是為了解決類型轉(zhuǎn)換的問題。我們使用了泛型,到頭來,還是要自己強(qiáng)轉(zhuǎn),違背了泛型設(shè)計(jì)的初衷。所以java不允許這么干。再說,你如果又用arrayList2往里面add()新的對象,那么到時(shí)候取得時(shí)候,我怎么知道我取出來的到底是String類型的,還是Object類型的呢?
?
?
所以,要格外注意,泛型中的引用傳遞的問題。
2、自動類型轉(zhuǎn)換
因?yàn)轭愋筒脸膯栴},所以所有的泛型類型變量最后都會被替換為原始類型。這樣就引起了一個(gè)問題,既然都被替換為原始類型,那么為什么我們在獲取的時(shí)候,不需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換呢?看下ArrayList和get方法:
?
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
看以看到,在return之前,會根據(jù)泛型變量進(jìn)行強(qiáng)轉(zhuǎn)。
?
寫了個(gè)簡單的測試代碼:
public class Test {
public static void main(String[] args) {
ArrayList<Date> list=new ArrayList<Date>();
list.add(new Date());
Date myDate=list.get(0);
}
然后反編了下字節(jié)碼,如下
public static void main(java.lang.String[]);
Code:
0: new #16 // class java/util/ArrayList
3: dup
4: invokespecial #18 // Method java/util/ArrayList."<init
:()V
7: astore_1
8: aload_1
9: new #19 // class java/util/Date
12: dup
13: invokespecial #21 // Method java/util/Date."<init>":()
16: invokevirtual #22 // Method java/util/ArrayList.add:(L
va/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokevirtual #26 // Method java/util/ArrayList.get:(I
java/lang/Object;
25: checkcast #19 // class java/util/Date
28: astore_2
29: return
看第22 ,它調(diào)用的是ArrayList.get()方法,方法返回值是Object,說明類型擦除了。然后第25,它做了一個(gè)checkcast操作,即檢查類型#19, 在在上面找#19引用的類型,他是
9: new #19 // class java/util/Date
是一個(gè)Date類型,即做Date類型的強(qiáng)轉(zhuǎn)。
所以不是在get方法里強(qiáng)轉(zhuǎn)的,是在你調(diào)用的地方強(qiáng)轉(zhuǎn)的。
?
附關(guān)于checkcast的解釋:
checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type. For example, if you write in Java:
return ((String)obj);
then the Java compiler will generate something like:
aload_1 ; push -obj- onto the stack
checkcast java/lang/String ; check its a String
areturn ; return it
checkcast is actually a shortand for writing Java code like:
if (! (obj == null || obj instanceof <class>)) {
throw new ClassCastException();
}
// if this point is reached, then object is either null, or an instance of
// <class> or one of its superclasses.
?
?
3、類型擦除與多態(tài)的沖突和解決方法
現(xiàn)在有這樣一個(gè)泛型類:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然后我們想要一個(gè)子類繼承它
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在這個(gè)子類中,我們設(shè)定父類的泛型類型為Pair<Date>,在子類中,我們覆蓋了父類的兩個(gè)方法,我們的原意是這樣的:
將父類的泛型類型限定為Date,那么父類里面的兩個(gè)方法的參數(shù)都為Date類型:“
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
?
所以,我們在子類中重寫這兩個(gè)方法一點(diǎn)問題也沒有,實(shí)際上,從他們的@Override標(biāo)簽中也可以看到,一點(diǎn)問題也沒有,實(shí)際上是這樣的嗎?
?
分析:
實(shí)際上,類型擦除后,父類的的泛型類型全部變?yōu)榱嗽碱愋蚈bject,所以父類編譯之后會變成下面的樣子:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
再看子類的兩個(gè)重寫的方法的類型:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
先來分析setValue方法,父類的類型是Object,而子類的類型是Date,參數(shù)類型不一樣,這如果實(shí)在普通的繼承關(guān)系中,根本就不會是重寫,而是重載。
我們在一個(gè)main方法測試一下:
public static void main(String[] args) throws ClassNotFoundException {
DateInter dateInter=new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object());//編譯錯誤
?}
如果是重載,那么子類中兩個(gè)setValue方法,一個(gè)是參數(shù)Object類型,一個(gè)是Date類型,可是我們發(fā)現(xiàn),根本就沒有這樣的一個(gè)子類繼承自父類的Object類型參數(shù)的方法。所以說,卻是是重寫了,而不是重載了。
?
為什么會這樣呢?
原因是這樣的,我們傳入父類的泛型類型是Date,Pair<Date>,我們的本意是將泛型類變?yōu)槿缦?#xff1a;
class Pair {
private Date value;
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
}
然后再子類中重寫參數(shù)類型為Date的那兩個(gè)方法,實(shí)現(xiàn)繼承中的多態(tài)。
可是由于種種原因,虛擬機(jī)并不能將泛型類型變?yōu)镈ate,只能將類型擦除掉,變?yōu)樵碱愋蚈bject。這樣,我們的本意是進(jìn)行重寫,實(shí)現(xiàn)多態(tài)。可是類型擦除后,只能變?yōu)榱酥剌d。這樣,類型擦除就和多態(tài)有了沖突。JVM知道你的本意嗎?知道!!!可是它能直接實(shí)現(xiàn)嗎,不能!!!如果真的不能的話,那我們怎么去重寫我們想要的Date類型參數(shù)的方法啊。
于是JVM采用了一個(gè)特殊的方法,來完成這項(xiàng)功能,那就是橋方法。
首先,我們用javap -c className的方式反編譯下DateInter子類的字節(jié)碼,結(jié)果如下:
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>"
:()V
4: return
public void setValue(java.util.Date); //我們重寫的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue
:(Ljava/lang/Object;)V
5: return
public java.util.Date getValue(); //我們重寫的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue
:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn
public java.lang.Object getValue(); //編譯時(shí)由編譯器生成的巧方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去調(diào)用我們重寫的getValue方法
;
4: areturn
public void setValue(java.lang.Object); //編譯時(shí)由編譯器生成的巧方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去調(diào)用我們重寫的setValue方法
)V
8: return
}
從編譯的結(jié)果來看,我們本意重寫setValue和getValue方法的子類,竟然有4個(gè)方法,其實(shí)不用驚奇,最后的兩個(gè)方法,就是編譯器自己生成的橋方法。可以看到橋方法的參數(shù)類型都是Object,也就是說,子類中真正覆蓋父類兩個(gè)方法的就是這兩個(gè)我們看不到的橋方法。而打在我們自己定義的setvalue和getValue方法上面的@Oveerride只不過是假象。而橋方法的內(nèi)部實(shí)現(xiàn),就只是去調(diào)用我們自己重寫的那兩個(gè)方法。
所以,虛擬機(jī)巧妙的使用了巧方法,來解決了類型擦除和多態(tài)的沖突。
不過,要提到一點(diǎn),這里面的setValue和getValue這兩個(gè)橋方法的意義又有不同。
setValue方法是為了解決類型擦除與多態(tài)之間的沖突。
而getValue卻有普遍的意義,怎么說呢,如果這是一個(gè)普通的繼承關(guān)系:
那么父類的setValue方法如下:
public ObjectgetValue() {
return super.getValue();
}
而子類重寫的方法是:
public Date getValue() {
return super.getValue();
}
其實(shí)這在普通的類繼承中也是普遍存在的重寫,這就是協(xié)變。
關(guān)于協(xié)變:。。。。。。
并且,還有一點(diǎn)也許會有疑問,子類中的巧方法? Object?? getValue()和Date getValue()是同 時(shí)存在的,可是如果是常規(guī)的兩個(gè)方法,他們的方法簽名是一樣的,也就是說虛擬機(jī)根本不能分別這兩個(gè)方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機(jī)卻是允許這樣做的,因?yàn)樘摂M機(jī)通過參數(shù)類型和返回類型來確定一個(gè)方法,所以編譯器為了實(shí)現(xiàn)泛型的多態(tài)允許自己做這個(gè)看起來“不合法”的事情,然后交給虛擬器去區(qū)別。
?
4、泛型類型變量不能是基本數(shù)據(jù)類型
不能用類型參數(shù)替換基本類型。就比如,沒有ArrayList<double>,只有ArrayList<Double>。因?yàn)楫?dāng)類型擦除后,ArrayList的原始類型變?yōu)镺bject,但是Object類型不能存儲double值,只能引用Double的值。
?
?
5、運(yùn)行時(shí)類型查詢
舉個(gè)例子:
ArrayList<String> arrayList=new ArrayList<String>();
因?yàn)轭愋筒脸?#xff0c;ArrayList<String>只剩下原始類型,泛型信息String不存在了。
那么,運(yùn)行時(shí)進(jìn)行類型查詢的時(shí)候使用下面的方法是錯誤的
if( arrayList instanceof ArrayList<String>)
java限定了這種類型查詢的方式
?
if( arrayList instanceof ArrayList<?>)? 是通配符的形式 ,將在后面一篇中介紹。
?
6、異常中使用泛型的問題
1、不能拋出也不能捕獲泛型類的對象。事實(shí)上,泛型類擴(kuò)展Throwable都不合法。例如:下面的定義將不會通過編譯:
public class Problem<T> extends Exception{......}為什么不能擴(kuò)展Throwable,因?yàn)楫惓6际窃谶\(yùn)行時(shí)捕獲和拋出的,而在編譯的時(shí)候,泛型信息全都會被擦除掉,那么,假設(shè)上面的編譯可行,那么,在看下面的定義:
try{
}catch(Problem<Integer> e1){
。。
}catch(Problem<Number> e2){
...
}
類型信息被擦除后,那么兩個(gè)地方的catch都變?yōu)樵碱愋蚈bject,那么也就是說,這兩個(gè)地方的catch變的一模一樣,就相當(dāng)于下面的這樣
try{
}catch(Problem<Object> e1){
。。
}catch(Problem<Object> e2){
...
這個(gè)當(dāng)然就是不行的。就好比,catch兩個(gè)一模一樣的普通異常,不能通過編譯一樣:
try{
}catch(Exception e1){
。。
}catch(Exception e2){//編譯錯誤
...
2、不能再catch子句中使用泛型變量
public static <T extends Throwable> void doWork(Class<T> t){
try{
...
}catch(T e){ //編譯錯誤
...
}
}
因?yàn)榉盒托畔⒃诰幾g的時(shí)候已經(jīng)變味原始類型,也就是說上面的T會變?yōu)樵碱愋蚑hrowable,那么如果可以再catch子句中使用泛型變量,那么,下面的定義呢:
public static <T extends Throwable> void doWork(Class<T> t){
try{
...
}catch(T e){ //編譯錯誤
...
}catch(IndexOutOfBounds e){
}
?}
根據(jù)異常捕獲的原則,一定是子類在前面,父類在后面,那么上面就違背了這個(gè)原則。即使你在使用該靜態(tài)方法的使用T是ArrayIndexOutofBounds,在編譯之后還是會變成Throwable,ArrayIndexOutofBounds是IndexOutofBounds的子類,違背了異常捕獲的原則。所以java為了避免這樣的情況,禁止在catch子句中使用泛型變量。
?
但是在異常聲明中可以使用類型變量。下面方法是合法的。
public static<T extends Throwable> void doWork(T t) throws T{
try{
...
}catch(Throwable realCause){
t.initCause(realCause);
throw t;
}
}
上面的這樣使用是沒問題的。
?
?
7、數(shù)組(這個(gè)不屬于類型擦除引起的問題)
不能聲明參數(shù)化類型的數(shù)組。如:
?
這是因?yàn)椴脸?#xff0c;table的類型變?yōu)镻air[],可以轉(zhuǎn)化成一個(gè)Object[]。
??
??數(shù)組可以記住自己的元素類型,下面的賦值會拋出一個(gè)ArrayStoreException異常。
???
??對于泛型而言,擦除降低了這個(gè)機(jī)制的效率。下面的賦值可以通過數(shù)組存儲的檢測,但仍然會導(dǎo)致類型錯誤。??
objarray =new Pair<Employee>();提示:如果需要收集參數(shù)化類型對象,直接使用ArrayList:ArrayList<Pair<String>>最安全且有效。
?
?
?
8、泛型類型的實(shí)例化?
不能實(shí)例化泛型類型。如,
first = new T(); //ERROR
???是錯誤的,類型擦除會使這個(gè)操作做成new Object()。
???不能建立一個(gè)泛型數(shù)組。
??
public<T> T[] minMax(T[] a){
T[] mm = new T[2]; //ERROR
...
}
???類似的,擦除會使這個(gè)方法總是構(gòu)靠一個(gè)Object[2]數(shù)組。但是,可以用反射構(gòu)造泛型對象和數(shù)組。
???利用反射,調(diào)用Array.newInstance:
publicstatic <T extends Comparable> T[]minmax(T[] a)
{
T[] mm == (T[])Array.newInstance(a.getClass().getComponentType(),2);
...
// 以替換掉以下代碼
// Obeject[] mm = new Object[2];
// return (T[]) mm;
}
?
9、類型擦除后的沖突
1、
當(dāng)泛型類型被擦除后,創(chuàng)建條件不能產(chǎn)生沖突。如果在Pair類中添加下面的equals方法:
class Pair<T> {
public boolean equals(T value) {
return null;
}
}
考慮一個(gè)Pair<String>。從概念上,它有兩個(gè)equals方法:
booleanequals(String); //在Pair<T>中定義
boolean equals(Object); //從object中繼承
但是,這只是一種錯覺。實(shí)際上,擦除后方法
boolean equals(T)
變成了方法 boolean equals(Object)
這與Object.equals方法是沖突的!當(dāng)然,補(bǔ)救的辦法是重新命名引發(fā)錯誤的方法。
?
2、
泛型規(guī)范說明提及另一個(gè)原則“要支持擦除的轉(zhuǎn)換,需要強(qiáng)行制一個(gè)類或者類型變量不能同時(shí)成為兩個(gè)接口的子類,而這兩個(gè)子類是同一接品的不同參數(shù)化。”
下面的代碼是非法的:
class Calendar implements Comparable<Calendar>{ ... } class GregorianCalendar extends Calendar implements Comparable<GregorianCalendar>{...} //ERRORGregorianCalendar會實(shí)現(xiàn)Comparable<Calender>和Compable<GregorianCalendar>,這是同一個(gè)接口的不同參數(shù)化實(shí)現(xiàn)。
這一限制與類型擦除的關(guān)系并不很明確。非泛型版本:
class Calendar implements Comparable{ ... } class GregorianCalendar extends Calendar implements Comparable{...} //ERROR是合法的。
?
?
10、泛型在靜態(tài)方法和靜態(tài)類中的問題
泛型類中的靜態(tài)方法和靜態(tài)變量不可以使用泛型類所聲明的泛型類型參數(shù)
舉例說明:
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
}
因?yàn)榉盒皖愔械姆盒蛥?shù)的實(shí)例化是在定義對象的時(shí)候指定的,而靜態(tài)變量和靜態(tài)方法不需要使用對象來調(diào)用。對象都沒有創(chuàng)建,如何確定這個(gè)泛型參數(shù)是何種類型,所以當(dāng)然是錯誤的。
但是要注意區(qū)分下面的一種情況:
public class Test2<T> {
public static <T >T show(T one){//這是正確的
return null;
}
}
因?yàn)檫@是一個(gè)泛型方法,在泛型方法中使用的T是自己在方法中定義的T,而不是泛型類中的T。
from:?https://blog.csdn.net/LonelyRoamer/article/details/7868820?
總結(jié)
以上是生活随笔為你收集整理的java泛型(二)、泛型的内部原理:类型擦除以及类型擦除带来的问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java 泛型中? super T和?
- 下一篇: Java泛型:类型擦除