首先,Java有泛型這一個(gè)概念,初衷是為了保證在運(yùn)行時(shí)出現(xiàn)的錯(cuò)誤能提早放到編譯時(shí)檢查。
1)Java泛型
開發(fā)人員在使用泛型的時(shí)候,很容易根據(jù)自己的直覺而犯一些錯(cuò)誤。比如一個(gè)方法如果接收List<Object>作為形式參數(shù),那么如果嘗試將一個(gè)List<String>的對(duì)象作為實(shí)際參數(shù)傳進(jìn)去,卻發(fā)現(xiàn)無法通過編譯。雖然從直覺上來說,Object是String的父類,這種類型轉(zhuǎn)換應(yīng)該是合理的。但是實(shí)際上這會(huì)產(chǎn)生隱含的類型轉(zhuǎn)換問題,因此編譯器直接就禁止這樣的行為。
2)實(shí)現(xiàn)方式:類型擦除
Java中的泛型基本上都是在編譯器這個(gè)層次來實(shí)現(xiàn)的,在生成的Java字節(jié)代碼中是不包含泛型中的類型信息的。
使用泛型的時(shí)候加上的類型參數(shù),會(huì)被編譯器在編譯的時(shí)候去掉,這個(gè)過程就稱為類型擦除。
如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會(huì)變成List。JVM看到的只是List,而由泛型附加的類型信息對(duì)JVM來說是不可見的。Java編譯器會(huì)在編譯時(shí)盡可能的發(fā)現(xiàn)可能出錯(cuò)的地方,但是仍然無法避免在運(yùn)行時(shí)刻出現(xiàn)類型轉(zhuǎn)換異常的情況。
很多泛型的奇怪特性都與這個(gè)類型擦除的存在有關(guān),包括:
泛型類并沒有自己獨(dú)有的Class類對(duì)象。比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
靜態(tài)變量是被泛型類的所有實(shí)例所共享的。對(duì)于聲明為MyClass<T>的類,訪問其中的靜態(tài)變量的方法仍然是 MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創(chuàng)建的對(duì)象,都是共享一個(gè)靜態(tài)變量。
泛型的類型參數(shù)不能用在Java異常處理的catch語句中。因?yàn)楫惓L幚硎怯蒍VM在運(yùn)行時(shí)刻來進(jìn)行的。由于類型信息被擦除,JVM是無法區(qū)分兩個(gè)異常類型MyException<String>和MyException<Integer>的。對(duì)于JVM來說,它們都是 MyException類型的。也就無法執(zhí)行與異常對(duì)應(yīng)的catch語句。
類型擦除的基本過程也比較簡(jiǎn)單,首先是找到用來替換類型參數(shù)的具體類。這個(gè)具體類一般是Object。如果指定了類型參數(shù)的上界的話,則使用這個(gè)上界。把代碼中的類型參數(shù)都替換成具體的類。同時(shí)去掉出現(xiàn)的類型聲明,即去掉<>的內(nèi)容。比如T get()方法聲明就變成了Object get();List<String>就變成了List。接下來就可能需要生成一些橋接方法(bridge method)。這是由于擦除了類型之后的類可能缺少某些必須的方法。比如考慮下面的代碼:
class MyString implements Comparable<String> {
public int compareTo(String str) {
return 0;
}
}
當(dāng)類型信息被擦除之后,上述類的聲明變成了
class MyString implements Comparable
但是這樣的話,類MyString就會(huì)有編譯錯(cuò)誤,因?yàn)闆]有實(shí)現(xiàn)接口Comparable聲明的int compareTo(Object)方法。這個(gè)時(shí)候就由編譯器來動(dòng)態(tài)生成這個(gè)方法。
3)泛型的檢測(cè):不符合泛型T的檢測(cè)
在進(jìn)行編譯之前就對(duì)所有泛型進(jìn)行檢測(cè),加入類型檢測(cè)和轉(zhuǎn)換的指令,比如返回泛型的結(jié)果實(shí)際上返回的是擦出后的類型,而虛擬機(jī)會(huì)多加一個(gè)類型轉(zhuǎn)換的指令。
4)需要泛型之間的類型轉(zhuǎn)換怎么做?通配符?與PECS
如果你是想遍歷collection,并對(duì)每一項(xiàng)元素操作時(shí),此時(shí)這個(gè)集合時(shí)生產(chǎn)者(生產(chǎn)元素),應(yīng)該使用 Collection<? extends E>,因?yàn)橄鄬?duì)于你泛型E,能放進(jìn)E中的(把Collection中的元素放入E類型中) ,應(yīng)該是E的子類型。
如果你是想添加元素到collection中去,那么此時(shí)集合時(shí)消費(fèi)者(消費(fèi)元素)應(yīng)該使用Collection<? super E>,同理,你要將E類的元素放入Collection中去,那么Collection應(yīng)該存放的是E的父類型。
5)泛型與多態(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è)子類中,我們?cè)O(shè)定父類的泛型類型為Pair<Date>,在子類中,我們覆蓋了父類的兩個(gè)方法,我們的原意是這樣的:
將父類的泛型類型限定為Date,那么父類里面的兩個(gè)方法的參數(shù)都為Date類型:
public Date getValue() {
return value;
}
public void setValue(Date value) {
this.value = value;
}
所以,我們?cè)谧宇愔兄貙戇@兩個(gè)方法一點(diǎn)問題也沒有,實(shí)際上,從他們的@Override標(biāo)簽中也可以看到,一點(diǎn)問題也沒有,實(shí)際上是這樣的嗎?
分析:
實(shí)際上,類型擦除后,父類的的泛型類型全部變?yōu)榱嗽碱愋蚈bject,所以父類編譯之后會(huì)變成下面的樣子:
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)系中,根本就不會(huì)是重寫,而是重載。
我們?cè)谝粋€(gè)main方法測(cè)試一下:
public static void main(String[] args) throws ClassNotFoundException {
DateInter dateInter=new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object());//編譯錯(cuò)誤
}
如果是重載,那么子類中兩個(gè)setValue方法,一個(gè)是參數(shù)Object類型,一個(gè)是Date類型,可是我們發(fā)現(xiàn),根本就沒有這樣的一個(gè)子類繼承自父類的Object類型參數(shù)的方法。所以說,卻是是重寫了,而不是重載了。
為什么會(huì)這樣呢?
原因是這樣的,我們傳入父類的泛型類型是Date,Pair<Date>,我們的本意是將泛型類變?yōu)槿缦拢?/p>
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)嗎,不能?。?!如果真的不能的話,那我們?cè)趺慈ブ貙懳覀兿胍腄ate類型參數(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 Object getValue() {
return super.getValue();
}
而子類重寫的方法是:
public Date getValue() {
return super.getValue();
}
其實(shí)這在普通的類繼承中也是普遍存在的重寫,這就是協(xié)變。
關(guān)于協(xié)變:。。。。。。
并且,還有一點(diǎn)也許會(huì)有疑問,子類中的巧方法 Object getValue()和Date getValue()是同 時(shí)存在的,可是如果是常規(guī)的兩個(gè)方法,他們的方法簽名是一樣的,也就是說虛擬機(jī)根本不能分別這兩個(gè)方法。如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的,但是虛擬機(jī)卻是允許這樣做的,因?yàn)樘摂M機(jī)通過參數(shù)類型和返回類型來確定一個(gè)方法,所以編譯器為了實(shí)現(xiàn)泛型的多態(tài)允許自己做這個(gè)看起來“不合法”的事情,然后交給虛擬器去區(qū)別。