了解泛型擦除嗎?知道類型擦除會造成多態的沖突嗎?如何解決?
泛型的代碼只存在于編譯階段,在進入JVM之前,與泛型相關的信息會被擦除掉,稱之為類型擦除。
- 無限制類型擦除:當在類的定義時沒有進行任何限制,那么在類型擦除后將會被替換成Object,例如<T>、<?> 都會被替換成Object。
- 有限制類型擦除:當類定義中的參數類型存在上下限(上下界),那么在類型擦除后就會被替換成類型參數所定義的上界或者下界,例如<? extend Person>會被替換成Person,而<? super Person> 則會被替換成Object。
泛型的橋接方法
類型擦除會造成多態的沖突,而JVM解決方法就是橋接方法。
舉例
現在有這樣一個泛型類:
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;
}
實際上,類型擦除后,父類的的泛型類型全部變為了原始類型Object,所以父類編譯之后會變成下面的樣子:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
再看子類的兩個重寫的方法的類型: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采用了一個特殊的方法,來完成這項功能,那就是橋方法。
原理
用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只不過是假象。而橋方法的內部實現,就只是去調用自己重寫的那兩個方法。
所以,虛擬機巧妙的使用了橋方法,來解決了類型擦除和多態的沖突。
并且,還有一點也許會有疑問,子類中的橋方法Object getValue()和Date getValue()是同時存在的,可是如果是常規的兩個方法,他們的方法簽名是一樣的,如果是我們自己編寫Java代碼,這樣的代碼是無法通過編譯器的檢查的(返回值不同不能作為重載的條件),但是虛擬機卻是允許這樣做的,因為虛擬機通過參數類型和返回類型來確定一個方法,所以編譯器為了實現泛型的多態允許自己做這個看起來“不合法”的事情,然后交給虛擬機去區別