泛型(二)->擦除&擦除帶來的問題
本篇首先介紹泛型的擦除,然後圍繞泛型擦除所帶來的問題進行精確打擊,話不多說,我們直接開始正文.
文中很多例子都會用到Pair這個對象,這裏統一聲明.
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;
}
}
泛型擦除
虛擬機中沒有泛型類型的對象–所有對象都是普通的類,無論我們什麼時候定義的泛型類型,在虛擬機中都自動轉換成了一個相應的原始類型.原始類型就是擦除類型變量,並替換爲限定符類型(沒有限定符用Object替換)後的泛型類型名.文字描述有點繞口,看下例子馬上就能明白.
例如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;
}
}
因爲T是無限定變量,所以全部替換成Object,看起來跟我們寫的普通類沒有什麼區別.
程序中我們可以寫出不同類別的Pair,比如Pair<String>
又或者Pair<Integer>
,而在虛擬機中擦除泛型後就變成原始類型的Pair了.
接下來我們具體說說泛型擦除的規則
泛型擦除就是類型變量用第一個限定來替換,如果沒有給定限定就用Object替換,例如類Pair<T>
中的類型變量沒有限定所以用Object替換,下面我們看一個聲明瞭類型變量限定的例子.
public class Interval<T extends Comparable & Serializable> {
private T lower;
private T upper;
public Interval(T first, T second) {
if (first.compareTo(second) > 0){
lower = second;
upper = first;
}else{
lower = first;
upper = second;
}
}
}
原始類型如下
public class Interval{
private Comparable lower;
private Comparable upper;
public Interval(Comparable first, Comparable second) {
}
}
看到這裏大家可能有個大膽的想法,把限定改爲Interval<T extends Serializable & Comparable>
會發生什麼,這樣做的話原始類型就用Serializable替換T,而編譯器會在需要的時候插入強制類型轉換爲Comparable,爲了提高效率,我們應該把沒有方法的接口放在後面.
翻譯泛型表達式
當調用泛型方法的時候,如果擦除返回值類型,編譯器將強制插入類型轉換.例如下面
Pair<Manager> pair = ...;
Manager manager = pair.getFirst();
擦除後getFirst返回值爲Object,編譯器會自動插入Manager強制類型轉換,所以getFirst()方法會執行如下兩個指令
- 對原始方法調用getFirst()
- 把返回值Object強轉成Manager
擦除所帶來的問題
泛型擦除與多態衝突
我們直接看例子
public class DateInterval extends Pair<Date> {
public void setSecond(Date date){
}
}
在擦除後變成
public class DateInterval extends Pair {
public void setSecond(Date date){
}
}
我們可以發現DateInterval中我們寫了一個setSecond(Date)方法,並且擦除後我們還從Pair繼承了一個setSecond(Obj)方法.那麼當我們調用setSecond()的時候會發生什麼呢.
是不是發現這跟我們想的不一樣啊,明明有兩個setSecond但是編譯器卻只提示了一個,也就是說這裏泛型擦除後與多態產生了衝突.在這種情況下編譯器會在DateInterval生成一個橋方法public void setSecond(Object second){setSecond((Date)second)}
,具體流程我們慢慢道來.
Pair聲明爲Pair<Date>
,而我們DateInterval中又寫了setSecond(Date)所以我們正常的理解這更應該是重寫對吧,同樣虛擬機也是這樣做的,泛型擦除後將爲原始類型的Pair生成一個橋方法public void setSecond(Object second){setSecond((Date)second)}
調用DateInterval的setSecond(Date),這樣就滿足我們需求了.當然這只是理論,接下來我們通過DateInterval.class文件來證明我們的觀點.
通過javap命令我們可以清楚的看到DateInterval中有個兩個setSecond()方法,並且setSecond(Obj)方法先把obj強轉成Date然後調用setSecond(Date)方法,印證了我們前面的理論是正確的.
當然我們還可能出現這種情況
public class DateInterval extends Pair<Date> {
public Date getSecond() {
return new Date();
}
}
泛型擦除後會有兩個方法
public Date getSecond() {}
public Object getSecond(){}
這在java代碼中是肯定不可能出現的,但是虛擬機中,用參數類型和返回類型確定一個方法,所以可能出現兩個返回值類型不同的方法字節碼,並且虛擬機能夠正確處理這個情況.
這裏稍加總結
- 虛擬機中沒有泛型,只有普通的類和方法.
- 所有的參數類型都用它們的類型限定符替換
- 橋方法被合成來保持多態
- 爲了保證類型安全,必要時會插入強制類型轉換.
泛型使用注意事項
不能用基本數據類型實例化類型參數
不能用基本數據類型替代類型變量,因此沒有Pair<int>
,只有Pair<Integer>
,其原因就是因爲類型擦除後,Pair類型變量會被替換成Object或者限定符,而它們不能存儲int.
運行時類型查詢只適用於原始類型
虛擬機中泛型對象都爲原始類型,所以運行時查詢只能比較原始類型.
像這樣將無法通過編譯.同樣道理,getClass方法總是返回原始類型.
Pair<String> p1 = new Pair();
Pair<Integer> p2 = new Pair();
if(p1.getClass() == p2.getClass())
比較結果爲true,因爲getClass方法返回的都是Pair.class
不允許創建參數化類型數組
不能創建參數化類型的數組,例如
Pair<String>[] pairs = new Pair<String>[2]; //error
因爲泛型擦除後pairs類型爲Pair[]
,然後我們可以轉換爲Object[]
Object[] o = pairs
然後在賦予新的值
o[0] = new Pair<Integer>();
能夠通過數組存儲檢查,但是當我們使用的時候肯定會產生錯誤,出於這個考慮不允許創建參數化類型數組.
需要注意的是隻是不允許創建,而聲明爲Pair<String>
是被允許的,只是不能實例化new Pair<String>[2]
.
泛型類的靜態上下文中類型變量無效
泛型變量屬於對應實例的,而靜態屬於類的根本沒法使用.下面例子是錯誤的
public class MyInstance<T> {
private static T t;//error
private static T getInstance(){//error
if(t == null){
}
return t;
}
}
擦除後的衝突
這個其實是上面泛型擦除後與多態衝突的另一種情況,我們把上面那個例子在改造下
public class DateInterval extends Pair<Date> {
public void setSecond(Object obj){
}
}
看了這個是不是感覺在故意搞事情
沒錯我們就是要搞到底,這樣當泛型擦除後會出現兩個public void setSecond(Object obj)
,那麼解決辦法只有一個就是重命名我們衝突的方法.
還有一種情況需要注意.如果兩個接口是同一個接口的不同參數化實現,那麼一個類或者類型變量不能同時成爲這兩個接口的子類型,書面表達非常拗口直接看例子.
class Calendar implements Comparable<Calendar>{}
class GregorianCalendar extends Calendar implement Comparable<GregorianCalendar>
GregorianCalendar會實現Comparable<Calendar>
和Comparable<GregorianCalendar>
,這是同一個接口的不同參數化.那麼合成的橋方法就可能產生衝突,例如如下情況
class GregorianCalendar extends Calendar implement Comparable<GregorianCalendar>{
public int compareTo(Calendar calendar){}
public int compareTo(GregorianCalendar gregorianCalendar){}
}
我們不可能合成public int compareTo(obj){compareTo((Calendar)(obj))}
和public int compareTo(obj){compareTo((GregorianCalendar)(obj))}
兩個橋方法這顯然不對.
到此泛型基本說完了,如有疑問歡迎留言.