Java泛型型別擦除以及型別擦除帶來的問題
一、Java泛型的實現方法:型別擦除
大家都知道,Java的泛型是偽泛型,這是因為Java在編譯期間,所有的泛型資訊都會被擦掉,正確理解泛型概念的首要前提是理解型別擦除。Java的泛型基本上都是在編譯器這個層次上實現的,在生成的位元組碼中是不包含泛型中的型別資訊的,使用泛型的時候加上型別引數,在編譯器編譯的時候會去掉,這個過程成為型別擦除。
如在程式碼中定義List<Object>
和List<String>
等型別,在編譯後都會變成List
,JVM看到的只是List
,而由泛型附加的型別資訊對JVM是看不到的。Java編譯器會在編譯時儘可能的發現可能出錯的地方,但是仍然無法在執行時刻出現的型別轉換異常的情況,型別擦除也是 Java 的泛型與 C++ 模板機制實現方式之間的重要區別。
通過兩個例子證明Java型別的型別擦除
1、原始型別相等
public class Test { public static void main(String[] args) { ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); } }
在這個例子中,我們定義了兩個ArrayList
陣列,不過一個是ArrayList<String>
泛型型別的,只能儲存字串;一個是ArrayList<Integer>
泛型型別的,只能儲存整數,最後,我們通過list1
物件和list2
物件的getClass()
方法獲取他們的類的資訊,最後發現結果為true
。說明泛型型別String
和Integer
都被擦除掉了,只剩下原始型別。
2、通過反射新增其它型別元素
public class Test { public static void main(String[] args) throws Exception { ArrayList<Integer> list = new ArrayList<Integer>(); list.add(1); //這樣呼叫 add 方法只能儲存整形,因為泛型型別的例項為 Integer list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i));//輸出:1 asd } } }
在程式中定義了一個ArrayList
泛型型別例項化為Integer
物件,如果直接呼叫add()
方法,那麼只能儲存整數資料,不過當我們利用反射呼叫add()
方法的時候,卻可以儲存字串,這說明了Integer
泛型例項在編譯之後被擦除掉了,只保留了原始型別。
二、型別擦除後保留的原始型別
在上面,兩次提到了原始型別,什麼是原始型別?
原始型別 就是擦除去了泛型資訊,最後在位元組碼中的型別變數的真正型別,無論何時定義一個泛型,相應的原始型別都會被自動提供,型別變數擦除,並使用其限定型別(無限定的變數用Object)替換。
1、原始型別Object
public class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
Pair的原始型別為:
public class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
因為在Pair<T>
中,T 是一個無限定的型別變數,所以用Object
替換,其結果就是一個普通的類,如同泛型加入Java語言之前的已經實現的樣子。在程式中可以包含不同型別的Pair
,如Pair<String>
或Pair<Integer>
,但是擦除型別後他們的就成為原始的Pair
型別了,原始型別都是Object
。
從上面的"一、2"中,我們也可以明白ArrayList<Integer>
被擦除型別後,原始型別也變為Object
,所以通過反射我們就可以儲存字串了。
如果型別變數有限定,那麼原始型別就用第一個邊界的型別變數類替換。
比如: Pair這樣宣告的話
public class Pair<T extends Comparable> {}
那麼原始型別就是Comparable
。
要區分原始型別和泛型變數的型別。
在呼叫泛型方法時,可以指定泛型,也可以不指定泛型。
- 在不指定泛型的情況下,泛型變數的型別為該方法中的幾種型別的同一父類的最小級,直到 Object。
- 在指定泛型的情況下,該方法的幾種型別必須是該泛型的例項的型別或者其子類。
public class Test {
public static void main(String[] args) {
/**不指定泛型的時候*/
int i = Test.add(1, 2); //這兩個引數都是Integer,所以T為Integer型別
Number f = Test.add(1, 1.2); //這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Number
Object o = Test.add(1, "asd"); //這兩個引數一個是Integer,以風格是Float,所以取同一父類的最小級,為Object
/**指定泛型的時候*/
int a = Test.<Integer>add(1, 2); //指定了Integer,所以只能為Integer型別或者其子類
int b = Test.<Integer>add(1, 2.2); //編譯錯誤,指定了Integer,不能為Float
Number c = Test.<Number>add(1, 2.2); //指定為Number,所以可以為Integer和Float
}
//這是一個簡單的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
其實在泛型類中,不指定泛型的時候,也差不多,只不過這個時候的泛型為Object
,就比如ArrayList
中,如果不指定泛型,那麼這個ArrayList
可以儲存任意的物件。
2、Object泛型
public static void main(String[] args) {
ArrayList list = new ArrayList();
list.add(1);
list.add("121");
list.add(new Date());
}
三、型別擦除引起的問題及解決方法
因為種種原因,Java不能實現真正的泛型,只能使用型別擦除來實現偽泛型,這樣雖然不會有型別膨脹問題,但是也引起來許多新問題,所以,SUN對這些問題做出了種種限制,避免我們發生各種錯誤。
1、先檢查再編譯以及編譯的物件和引用傳遞問題
Q: 既然說型別變數會在編譯的時候擦除掉,那為什麼我們往 ArrayList 建立的物件中新增整數會報錯呢?不是說泛型變數String會在編譯的時候變為Object型別嗎?為什麼不能存別的型別呢?既然型別擦除了,如何保證我們只能使用泛型變數限定的型別呢?
A: Java編譯器是通過先檢查程式碼中泛型的型別,然後在進行型別擦除,再進行編譯。
例如:
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);//編譯錯誤
}
在上面的程式中,使用add
方法新增一個整型,在IDE中,直接會報錯,說明這就是在編譯之前的檢查,因為如果是在編譯之後檢查,型別擦除後,原始型別為Object
,是應該允許任意引用型別新增的。可實際上卻不是這樣的,這恰恰說明了關於泛型變數的使用,是會在編譯之前檢查的。
那麼,這個型別檢查是針對誰的呢?我們先看看引數化型別和原始型別的相容。
以 ArrayList舉例子,以前的寫法:
ArrayList list = new ArrayList();
現在的寫法:
ArrayList<String> list = new ArrayList<String>();
如果是與以前的程式碼相容,各種引用傳值之間,必然會出現如下的情況:
ArrayList<String> list1 = new ArrayList(); //第一種 情況
ArrayList list2 = new ArrayList<String>(); //第二種 情況
這樣是沒有錯誤的,不過會有個編譯時警告。
不過在第一種情況,可以實現與完全使用泛型引數一樣的效果,第二種則沒有效果。
因為型別檢查就是編譯時完成的,new ArrayList()
只是在記憶體中開闢了一個儲存空間,可以儲存任何型別物件,而真正設計型別檢查的是它的引用,因為我們是使用它引用list1
來呼叫它的方法,比如說呼叫add
方法,所以list1
引用能完成泛型型別的檢查。而引用list2
沒有使用泛型,所以不行。
舉例子:
public class Test {
public static void main(String[] args) {
ArrayList<String> list1 = new ArrayList();
list1.add("1"); //編譯通過
list1.add(1); //編譯錯誤
String str1 = list1.get(0); //返回型別就是String
ArrayList list2 = new ArrayList<String>();
list2.add("1"); //編譯通過
list2.add(1); //編譯通過
Object object = list2.get(0); //返回型別就是Object
new ArrayList<String>().add("11"); //編譯通過
new ArrayList<String>().add(22); //編譯錯誤
String str2 = new ArrayList<String>().get(0); //返回型別就是String
}
}
通過上面的例子,我們可以明白,型別檢查就是針對引用的,誰是一個引用,用這個引用呼叫泛型方法,就會對這個引用呼叫的方法進行型別檢測,而無關它真正引用的物件。
泛型中引數話型別為什麼不考慮繼承關係?
在Java中,像下面形式的引用傳遞是不允許的:
ArrayList<String> list1 = new ArrayList<Object>(); //編譯錯誤
ArrayList<Object> list2 = new ArrayList<String>(); //編譯錯誤
我們先看第一種情況,將第一種情況拓展成下面的形式:
ArrayList<Object> list1 = new ArrayList<Object>();
list1.add(new Object());
list1.add(new Object());
ArrayList<String> list2 = list1; //編譯錯誤
實際上,在第4行程式碼的時候,就會有編譯錯誤。那麼,我們先假設它編譯沒錯。那麼當我們使用list2
引用用get()
方法取值的時候,返回的都是String
型別的物件(上面提到了,型別檢測是根據引用來決定的),可是它裡面實際上已經被我們存放了Object
型別的物件,這樣就會有ClassCastException
了。所以為了避免這種極易出現的錯誤,Java不允許進行這樣的引用傳遞。(這也是泛型出現的原因,就是為了解決型別轉換的問題,我們不能違背它的初衷)。
再看第二種情況,將第二種情況拓展成下面的形式:
ArrayList<String> list1 = new ArrayList<String>();
list1.add(new String());
list1.add(new String());
ArrayList<Object> list2 = list1; //編譯錯誤
沒錯,這樣的情況比第一種情況好的多,最起碼,在我們用list2
取值的時候不會出現ClassCastException
,因為是從String
轉換為Object
。可是,這樣做有什麼意義呢,泛型出現的原因,就是為了解決型別轉換的問題。我們使用了泛型,到頭來,還是要自己強轉,違背了泛型設計的初衷。所以java不允許這麼幹。再說,你如果又用list2
往裡面add()
新的物件,那麼到時候取得時候,我怎麼知道我取出來的到底是String
型別的,還是Object
型別的呢?
所以,要格外注意,泛型中的引用傳遞的問題。
2、自動型別轉換
因為型別擦除的問題,所以所有的泛型型別變數最後都會被替換為原始型別。
既然都被替換為原始型別,那麼為什麼我們在獲取的時候,不需要進行強制型別轉換呢?
看下ArrayList.get()
方法:
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
可以看到,在return
之前,會根據泛型變數進行強轉。假設泛型型別變數為Date
,雖然泛型資訊會被擦除掉,但是會將(E) elementData[index]
,編譯為(Date) elementData[index]
。所以我們不用自己進行強轉。當存取一個泛型域時也會自動插入強制型別轉換。假設Pair
類的value
域是public
的,那麼表示式:
Date date = pair.value;
也會自動地在結果位元組碼中插入強制型別轉換。
3、型別擦除與多型的衝突和解決方法
現在有這樣一個泛型類:
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
然後我們想要一個子類繼承它。
class DateInter extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
在這個子類中,我們設定父類的泛型型別為Pair<Date>
,在子類中,我們覆蓋了父類的兩個方法,我們的原意是這樣的:將父類的泛型型別限定為Date
,那麼父類裡面的兩個方法的引數都為Date
型別。
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
所以,我們在子類中重寫這兩個方法一點問題也沒有,實際上,從他們的@Override
標籤中也可以看到,一點問題也沒有,實際上是這樣的嗎?
分析:實際上,型別擦除後,父類的的泛型型別全部變為了原始型別Object
,所以父類編譯之後會變成下面的樣子:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
再看子類的兩個重寫的方法的型別:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
先來分析setValue
方法,父類的型別是Object
,而子類的型別是Date
,引數型別不一樣,這如果實在普通的繼承關係中,根本就不會是重寫,而是過載。
我們在一個main方法測試一下:
public static void main(String[] args) throws ClassNotFoundException {
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object()); //編譯錯誤
}
如果是過載,那麼子類中兩個setValue
方法,一個是引數Object
型別,一個是Date
型別,可是我們發現,根本就沒有這樣的一個子類繼承自父類的Object型別引數的方法。所以說,卻是是重寫了,而不是過載了。
為什麼會這樣呢?
原因是這樣的,我們傳入父類的泛型型別是Date,Pair<Date>
,我們的本意是將泛型類變為如下:
class Pair {
private Date value;
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
}
然後再子類中重寫引數型別為Date的那兩個方法,實現繼承中的多型。
可是由於種種原因,虛擬機器並不能將泛型型別變為Date
,只能將型別擦除掉,變為原始型別Object
。這樣,我們的本意是進行重寫,實現多型。可是型別擦除後,只能變為了過載。這樣,型別擦除就和多型有了衝突。JVM知道你的本意嗎?知道!!!可是它能直接實現嗎,不能!!!如果真的不能的話,那我們怎麼去重寫我們想要的Date
型別引數的方法啊。
於是JVM採用了一個特殊的方法,來完成這項功能,那就是橋方法。
首先,我們用javap -c className
的方式反編譯下DateInter
子類的位元組碼,結果如下:
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(); //編譯時由編譯器生成的橋方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去呼叫我們重寫的getValue方法;
4: areturn
public void setValue(java.lang.Object); //編譯時由編譯器生成的橋方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去呼叫我們重寫的setValue方法)V
8: return
}
從編譯的結果來看,我們本意重寫setValue
和getValue
方法的子類,竟然有4個方法,其實不用驚奇,最後的兩個方法,就是編譯器自己生成的橋方法。可以看到橋方法的引數型別都是Object,也就是說,子類中真正覆蓋父類兩個方法的就是這兩個我們看不到的橋方法。而在我們自己定義的setvalue
和getValue
方法上面的@Oveerride
只不過是假象。而橋方法的內部實現,就只是去呼叫我們自己重寫的那兩個方法。
所以,虛擬機器巧妙的使用了橋方法,來解決了型別擦除和多型的衝突。
不過,要提到一點,這裡面的setValue
和getValue
這兩個橋方法的意義又有不同。
setValue
方法是為了解決型別擦除與多型之間的衝突。
而getValue
卻有普遍的意義,怎麼說呢,如果這是一個普通的繼承關係:
那麼父類的getValue
方法如下:
public Object getValue() {
return value;
}
而子類重寫的方法是:
public Date getValue() {
return super.getValue();
}
其實這在普通的類繼承中也是普遍存在的重寫,這就是協變。
關於協變:。。。。。。
並且,還有一點也許會有疑問,子類中的橋方法Object getValue()
和Date getValue()
是同時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機器根本不能分別這兩個方法。如果是我們自己編寫Java程式碼,這樣的程式碼是無法通過編譯器的檢查的,但是虛擬機器卻是允許這樣做的,因為虛擬機器通過引數型別和返回型別來確定一個方法,所以編譯器為了實現泛型的多型允許自己做這個看起來“不合法”的事情,然後交給虛擬器去區別。
4、泛型型別變數不能是基本資料型別
不能用型別引數替換基本型別。就比如,沒有ArrayList<double>
,只有ArrayList<Double>
。因為當型別擦除後,ArrayList
的原始型別變為Object
,但是Object
型別不能儲存double
值,只能引用Double
的值。
5、編譯時集合的instanceof
ArrayList<String> arrayList = new ArrayList<String>();
因為型別擦除之後,ArrayList<String>
只剩下原始型別,泛型資訊String
不存在了。
那麼,編譯時進行型別查詢的時候使用下面的方法是錯誤的
if( arrayList instanceof ArrayList<String>)
6、泛型在靜態方法和靜態類中的問題
泛型類中的靜態方法和靜態變數不可以使用泛型類所宣告的泛型型別引數
舉例說明:
public class Test2<T> {
public static T one; //編譯錯誤
public static T show(T one){ //編譯錯誤
return null;
}
}
因為泛型類中的泛型引數的例項化是在定義物件的時候指定的,而靜態變數和靜態方法不需要使用物件來呼叫。物件都沒有建立,如何確定這個泛型引數是何種型別,所以當然是錯誤的。
但是要注意區分下面的一種情況:
public class Test2<T> {
public static <T >T show(T one){ //這是正確的
return null;
}
}
因為這是一個泛型方法,在泛型方法中使用的T是自己在方法中定義的 T,而不是泛型類中的T。