虛擬機沒有泛型類型對象。所有對象都屬於普通類。
類型擦除
無論何時定義一個泛型類型,都自動提供了一個相應的原始類型(raw type)。原始類型的名字就是刪去類型參數後的泛型類型名。擦除(erased)類型變量,並替換爲限定類型(無限定的變量用Object)。
例如,Pair<T>
的原始類型如下所示:
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() {return first;}
public Object getSecond() {return second;}
public void setFirst(Object newValue) {first = newValue;}
public void setSecond(Object newValue) {second = newValue;}
}
因爲T
是一個無限定的變量,所以直接用Object替換。
結果是一個普通的類,就好像泛型引入Java語言之前已經實現的那樣。
在程序中可以包含不同類型的Pair
,例如,Pair<String>
或Pair<LocalDate>
。而擦除類型後就變成原始的Pair
類型了。
原始類型用第一個限定的類型變量來替換,如果沒有給定限定就用Object替換。例如,類Pair<T>
中的類型變量沒有顯示的限定,因此,原始類型用Object
替換T
。假定聲明瞭一個不同的類型。
public class Interval<T extends Comparable & Serializable> implements Serializable {
private T lower;
private T upper;
...
public Inteval(T first, T second) {
if (first.compareTo(second) <= 0) {
lower = first;
upper = second;
} else {
lower = second;
upper = first;
}
}
}
原始類型Interval
如下:
public class Interval implements Serializable {
private Comparable lower;
private Comparable upper;
...
public Interval(Comparable first, Comparable second){...}
}
翻譯泛型表達式
當程序調用泛型方法時,如果擦除返回類型,編譯器插入強制類型轉換。如:
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
擦除getFirst的返回類型後將返回Object
類型,編譯器自動插入Employee的強制類型轉換。也就是說,編譯器把這個方法調用翻譯爲兩條虛擬機指令:
- 對原始方法
Pair.getFirst
的調用。 - 將返回的
Object
類型強制轉換爲Employee類型。
當存取一個泛型域時也要插入強制類型轉換。假設Pair
類的first域和second域都是共有的。表達式:
Employee buddy = buddies.first;
也會在結果字節碼中插入強制強制類型轉換。
翻譯泛型方法
類型擦除也會出現在泛型方法中。程序員通常認爲這個泛型方法:
public static <T extends Comparable> T min(T[] a)
是一個完整的方法族,而擦除類型之後,只剩下一個方法:
public static Comparable min(Comparable[] a);
注意,類型參數T
已經被擦除了,只留下了限定類型Comparable
。
方法的擦除帶來了兩個複雜問題。如:
class DateInterval extends Pair<LocalDate> {
public void setSecond(LocalDate second) {
if (second.compareTo(getFirst()) >= 0)
super.setSecond(second);
}
}
一個日期區間是一對LocalDate
對象,並且需要覆蓋這個方法來確保第二個永遠不小於第一個值。這個類擦除後:
class DateInterval extends Pair {
public void setSecond(LocalDate second){...}
}
令人感到奇怪的是,存在另一個從Pair繼承的setSecond方法,即
public void setSecond(Object second)
這顯然是一個不同的方法,因爲它有一個不同的類型參數Object
,而不是LocalDate
。然而,不應該不一樣。考慮下面的語句序列:
DateInterval interval = new DateInterval(...);
Pair<LocalDate> pair = interval;
pair.setSecond(aDate);
這裏,希望對setSecond
的調用具有多態性,並調用最合適的那個方法。由於pair
引用DateInterval
對象,所以應該調用DateInterval.setSecond
。問題在於類型擦除與多態發生了衝突。要解決這個問題,就需要編譯器在DateInterval
類中生成一個橋方法(bridge method)。
public void setSecond(Object second){
setSecond((Date)second);
}
想要了解它的工作過程,請仔細地跟蹤下列語句的執行:
pair.setSecond(aDate);
變量pair
已經聲明爲類型Pair<LocalDate>
,並且這個類型只有一個簡單的方法叫setSecond
,即setSecond(Object)
。虛擬機用pair
引用的對象調用這個方法。這個對象是DateInterval
類型的,因而將會調用DateInterval.setSecond(Object)
方法。這個方法是合成的橋方法。它調用DateInterval.setSecond(Date)
,這正是我們所期望的操作效果。
橋方法可能會變得十分奇怪。假設DateInterval
方法也覆蓋了getSecond
方法:
class DateInterval extends Pair<LocalDate> {
public LocalDate getSecond() {
return (Date)super.getSecond().clone();
}
}
在DateInterval
類中,有兩個getSecond方法:
LocalDate getSecond();
Object getSecond();
不能這樣編寫Java代碼(具有相同參數類型的兩個方法是不合法的)。它們都沒有參數。但是,在虛擬機中,用參數類型和返回類型確定一個方法。因此,編譯器可能產生兩個僅返回類型不同的方法字節碼,虛擬機能夠正確地處理這一情況。
總之,需要記住有關Java泛型轉換的事實:
- 虛擬機中沒有泛型,只有普通的類和方法。
- 所有的類型參數都用它們的限定類型替換。
- 橋方法被合成來保持多態。
- 爲保持類型安全性,必要時插入強制類型轉換。
捐贈
若你感覺讀到這篇文章對你有啓發,能引起你的思考。請不要吝嗇你的錢包,你的任何打賞或者捐贈都是對我莫大的鼓勵。