一、Java泛型的實現方法:類型擦除
在最開始的時候已經簡單介紹了一下,現在在回顧一下:
泛型思想早在C++語言的模板(Templates)中就開始生根發芽,在Java語言處於還沒有出現泛型的版本時,只能通過Object是所有類型的父類和類型強制轉換兩個特點的配合來實現類型泛化。例如在哈希表的存取中,JDK 1.5之前使用HashMap的get()方法,返回值就是一個Object對象,由於Java語言裏面所有的類型都繼承於java.lang.Object,那Object轉型成任何對象都是有可能的。但是也因爲有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object到底是個什麼類型的對象。在編譯期間,編譯器無法檢查這個Object的強制轉型是否成功,如果僅僅依賴程序員去保障這項操作的正確性,許多ClassCastException的風險就會被轉嫁到程序運行期之中。
泛型技術在C#和Java之中的使用方式看似相同,但實現上卻有着根本性的分歧,C#裏面泛型無論在程序源碼中、編譯後的IL中(Intermediate Language,中間語言,這時候泛型是一個佔位符)或是運行期的CLR中都是切實存在的,List<int>與List<String>就是兩個不同的類型,它們在系統運行期生成,有自己的虛方法表和類型數據,這種實現稱爲類型膨脹,基於這種方法實現的泛型被稱爲真實泛型。
Java語言中的泛型則不一樣,它只在程序源碼中存在,在編譯後的字節碼文件中,就已經被替換爲原來的原生類型(Raw Type,也稱爲裸類型)了,並且在相應的地方插入了強制轉型代碼,因此對於運行期的Java語言來說,ArrayList<int>與ArrayList<String>就是同一個類。所以說泛型技術實際上是Java語言的一顆語法糖,Java語言中的泛型實現方法稱爲類型擦除,基於這種方法實現的泛型被稱爲僞泛型。
正確理解泛型概念的首要前提是理解類型擦出(type erasure)。Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會在編譯器在編譯的時候去掉。這個過程就稱爲類型擦除。如在代碼中定義的List<object>和List<String>等類型,在編譯後都會編程List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。Java編譯器會在編譯時儘可能的發現可能出錯的地方,但是仍然無法避免在運行時刻出現類型轉換異常的情況。類型擦除也是Java的泛型實現方法與C++模版機制實現方式之間的重要區別。
很多泛型的奇怪特性都與這個類型擦除的存在有關,包括:
- 泛型類並沒有自己獨有的Class類對象。比如並不存在List<String>.class或是List<Integer>.class,而只有List.class。
- 靜態變量是被泛型類的所有實例所共享的。對於聲明爲MyClass<T>的類,訪問其中的靜態變量的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創建的對象,都是共享一個靜態變量。
- 泛型的類型參數不能用在Java異常處理的catch語句中。因爲異常處理是由JVM在運行時刻來進行的。由於類型信息被擦除,JVM是無法區分兩個異常類型MyException<String>和MyException<Integer>的。對於JVM來說,它們都是 MyException類型的。也就無法執行與異常對應的catch語句。
可以通過兩個簡單的例子,來證明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());
}
}
在這個例子中,我們定義了兩個ArrayList數組,不過一個是ArrayList<String>泛型類型,只能存儲字符串。一個是ArrayList<Integer>泛型類型,只能存儲整形。最後,我們通過arrayList1對象和arrayList2對象的getClass方法獲取它們的類的信息,最後發現結果爲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);//這樣調用add方法只能存儲整形,因爲泛型類型的實例爲Integer
arrayList3.getClass().getMethod("add", Object.class).invoke(arrayList3, "asd");
for (int i=0;i<arrayList3.size();i++) {
System.out.println(arrayList3.get(i));
}
}
在程序中定義了一個ArrayList泛型類型實例化爲Integer的對象,如果直接調用add方法,那麼只能存儲整形的數據。不過當我們利用反射調用add方法的時候,卻可以存儲字符串。這說明了Integer泛型實例在編譯之後被擦除了,只保留了原型類型。
二、類型擦除後保留的原始類型
在上面,兩次提到了原始類型,什麼是原始類型?原始類型(raw type)就是擦除去了類型參數的泛型類型的名字。無論何時定義一個泛型類型,相應的原始類型都會被自動地提供。類型變量被擦除(crased),並使用其限定類型(無限定的變量用Object)替換。
比如:
例3:
public class Pair<T> {
private T first;
private T second;
public Pair() {
first=null;
second=null;
}
public Pair(T first,T second){
this.first=first;
this.second=second;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
}
Pair<T>的原始類型爲:
public class Pair {
private Object first;
private Object second;
public Pair() {
first=null;
second=null;
}
public Pair(Object first,Object second){
this.first=first;
this.second=second;
}
public Object getFirst() {
return first;
}
public void setFirst(Object first) {
this.first = first;
}
public Object getSecond() {
return second;
}
public void setSecond(Object second) {
this.second = second;
}
}
因爲在Pair<T>中,T是一個無限定的類型變量,所以用Object替換。其結果就是一個普通的類,如同泛型加入java變成語言之前已經實現的那樣。在程序中可以包含不同類型的Pair,如Pair<String>或Pair<Integer>,但是,擦除類型後它們就成爲原始的Pair類型了。
從上面的那個例2中,我們也可以明白ArrayList<Integer>被擦除類型後,原始類型也變成了Object,所以通過反射我們就可以存儲字符串了。
如果類型變量有限定,那麼原始類型就用第一個邊界的類型變量來替換。
比如Pair這樣聲明
例4:
public class Pair<T extends Comparable& Serializable> {
那麼原始類型就是Comparable
注意:
如果Pair這樣聲明public class Pair<T extends Serializable&Comparable> ,那麼原始類型就用Serializable替換,而編譯器在必要的時要向Comparable插入強制類型轉換。爲了提高效率,應該將標籤(tagging)接口(即沒有方法的接口)放在邊界限定列表的末尾。
三、翻譯泛型表達式
因爲類型擦除的問題,所以所有的泛型類型變量最後都會被替換爲原始類型。這樣就引起了一個問題,既然都被替換爲原始類型,那麼爲什麼我們在獲取的時候,不需要進行強制類型轉換呢?就比如ArrayList<String>存儲了一些字符串,而它被類型擦除後,原始類型爲Object,那麼爲什麼我們獲取的時候,可以直接以String str=arrayList.get(1)這樣的形式獲取呢?
這是因爲當程序調用這些泛型方法的時候,如果返回類型被擦除了,編譯器會自動插入強制類型轉換。
以上面例3的Pair<T>舉例說明:
例5:
Pair<Date> pair=new Pair<Date>();
Date date=pair.getFirst();
getFirst的讀出將返回Object類型。編譯器會自動插入Date的強制類型轉換,而不需要我們自己去強轉。
當存取一個泛型域時也會自動插入強制類型轉換。假設Pair類的first和second域都是public的,那麼,表達式:
Date date=pair.first
也會自動地在結果字節碼中插入強制類型轉換。
四、翻譯泛型方法(重點)
類型擦除也會出現在泛型方法中。
1、先檢查,在編譯
理解泛型方法,首先要明白java編譯器會先檢查代碼中泛型的類型,然後再進行類型擦除,在進行編譯的。
舉個例子說明:
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("123");
arrayList.add(123);//編譯錯誤
}
在上面的程序中,使用add方法添加一個整形,在eclipse中,直接就會報錯,說明這就是在編譯之前的檢查。因爲如果是在編譯之後檢查,類型擦除後,原始類型爲Object,是應該運行任意引用類型的添加的。可實際上卻不是這樣,這恰恰說明了關於泛型變量的使用,是會在編譯之前檢查的。
2、類型擦除與多態的衝突和解決方法
現在有這樣一個泛型類:
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卻有普遍的意義,怎麼說呢,如果這是一個普通的繼承關係:
那麼父類的setValue方法如下:
public ObjectgetValue() {
return super.getValue();
}
而子類重寫的方法是:
public Date getValue() {
return super.getValue();
}
其實這在普通的類繼承中也是普遍存在的重寫,這就是協變。
關於協變:。。。。。。
並且,還有一點也許會有疑問,子類中的巧方法 Object getValue()和Date getValue()是同 時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,也就是說虛擬機根本不能分別這兩個方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機卻是允許這樣做的,因爲虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器爲了實現泛型的多態允許自己做這個看起來“不合法”的事情,然後交給虛擬器去區別。