虛擬機沒有泛型類對象——所有對象都屬於普通類。所以泛型類會在編譯時被翻譯成普通類。
類型擦除
無論何時定義一個泛型類型,都自動提供了一個相應的原始類型(raw type)。原始類型的名字就是刪除類型參數後的泛型類型名。擦除(erased)類型變量,並替換爲限定類型(無線定的變量用Object)。
例如,Pair的原始類型如下所示:
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語言之前的實現是類似的。
**原始類型用第一個限定的類型變量來替換,如果沒有給定限定用Object替換。**如果聲明瞭一個不同的類型:
public class Interval(T extends Comparable & Serizalizable> implements Serializable
{
private T lower;
private T upper;
...
public Interval(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) { ... }
}
如果將Interavel切換限定爲:class Interavel<T extends Serializable & Comparable>
,原始類型將用Serializable
替換T
,編譯器在必要時要向Comparable
插入強制類型轉換。爲了提高效率,應該將標籤(tagging)接口放在便捷列表的末尾。
翻譯泛型表達式
當程序調用泛型方法時,如果擦除返回類型,編譯器插入強制類型轉換。例如:
Pair<Employee> budies = ...;
Employee buddy = (Employee) buddies.getFirst();
當存取一個泛型域時也要插入強制類型轉換。例如:
Employee buddy = (Employee) 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);
}
}
...
}
在DateInterval
類中,我們想覆蓋Pair
的setSecond
方法來確保第二個值永遠不小於第一個值。DateInterval
類擦除後變成:
class DateInterval extends Pair //after erasure
{
public void setSecond((LocalDate second) { ... }
...
}
這個時候問題就出來了,我們發現DateInterval
類中的setSecond
方法參數與Pair
類中的setSecond
方法參數不一樣。這樣,DateInterval
類就有兩個setSecond
方法:
// 自己的
public void setSecond(LocalDate second) { ... }
// 從Pair父類中繼承的
public void setSecond(Object second) { ... }
我們原本想通過在DateInterval
類中重寫父類Pair
的setSecond
方法來實現繼承多態性,可是類型擦除後,變成了重載。考慮一下代碼:
Pair<LocalDate> pair = new DateInterval(...);
pair.setSecond(LocalDate.now());
pair
是Pair<LocalDate>
類型,當在pair
對象上調用setSecond(LocalDate.now())
時,會執行Pair
的setSecond(Object second)
方法,由於pair
引用DateInterval
對象,如果DateInterval
類的setSecond
方法覆蓋了父類的該方法,應該調用DateInterval.setSecond(LocalDate)
。但DateInerval
沒有重寫而是重載了Pair.setSecond(Object)
方法,也就是說是類型擦除與多態發送了衝突。(更多說明參見:【java】–泛型-類型擦除與多態的衝突和解決方法_Java_qfzhangwei的專欄-CSDN博客)
要解決這個問題,就需要編譯器在DateInterval
類中生成一個橋方法(bridge method):
public void setSecond(Object second)
{
setSecond((Date) second);
}
(編譯器在編譯階段自動生成)
跟蹤下列語句的執行:
pair.setSecond(LocalDate.now());
變量pair
已經聲明爲類型Pair<LocalDate>
,並且這個類型只有一個簡單的方法叫setSecond
,即setSecond(Object)
。虛擬機用pair
引用的對象調用這個方法。這個對象是DateInterval
類型的,因而將會調用DateInterval.setSecond(Object)
方法。這個方法是合成的橋方法。它調用DateInterval.setSecond(Date)
,這正是我們所期望的操作效果。
橋方法可能會變得十分奇怪。假設DateInerval
也覆蓋了getSecond
方法:
class DateInterval extends Pair<LocalDate>
{
public LocalDate getSecond() { ... }
...
}
在DateInterval
類中,有兩個getSecond
方法:
LocalDate getSecond() // defined in DateInterval
Object getSecond() // overrides the method defined in Pair to call the first method
我們不能這樣編寫Java代碼,它們有相同的方法名稱且都沒有參。但是,在虛擬機中,是用參數類型和返回類型確定一個方法的。因此,編譯可能產生兩個僅返回類型不同的方法字節碼,虛擬機能夠正確地處理這一情況。
橋方法不僅用於泛型類型。在一個方法覆蓋另一個方法時可以指定一個更嚴格的返回類型。例如:
public class Employee implements Cloneable
{
public Employee clone() throws CloneNotSupportedException { ... }
Object.clone
和Employee.clone
方法被說成具有協變的返回類型(covariant return types)。實際上,Employee
類有兩個克隆方法:
Employee clone() // defaine above
Object clone() // synthesized bridge method, overrides Object.clone
合成的橋方法調用了新定義的方法。
總之,需要記住有關Java泛型轉換的事實:
- 虛擬機中沒有泛型,只有普通的類和方法。
- 所有的類型參數都用它們的限定類型替換。
- 橋方法被合成來保持多臺。
- 爲保持類型安全性,必要時插入強制類型轉換。
調用遺留代碼
設計Java泛型類型時,主要目標是允許泛型代碼和遺留代碼之間能互操作。
下面示例中,要想設置一個JSlider標籤,可以使用方法:
void setLabelTable(Dictionary table)
這裏的Dictionary
是一個原始類型,因爲實現JSlider
類時Java中還不存在泛型。不過,填充Dictionary
時,要使用泛型類型。
Dictionary<Integer, Component> labelTable = new Hashtable<>();
lableTable.put(0, new JLable(new ImageIcon("nine.gif")));
lableTable.put(20, new JLable(new ImageIcon("ten.gif")));
...
將Dictionary<Integer, Component>
對象傳遞給setLabelTable
時,編譯器會發送一個警告。
slider.setLabelTable(labelTable); // Warning
畢竟,編譯器無法確定setLabelTable
可能會對Dictionary
對象做什麼操作。這個方法可能會用字符串替換所有的關鍵字。這就打破了關鍵字類型爲整數(Integer)的承諾,未來的操作有可能會產生強制類型轉換的異常。
這個警告對操作不會產生什麼影響,最多考慮一下JSlider
有可能用Dictionary
·對象做什麼就可以了。這裏十分清楚,JSlider
只閱讀這個信息,因此可以忽略這個警告。
在查看了警告之後,可以利用註解(annotation)使之消失。註解必須放在生成這個警告的代碼所在的方法之前,如下:
@SuppressWarning("unchecked")
Dictionary<Integer, Components> labelTable = slider.getLabelTable(); // No warning
或者,可以標註整個方法,這個註解會關閉對方法中所有代碼的檢查。