1. 程式人生 > >CoreJava讀書筆記--泛型程式設計(二)

CoreJava讀書筆記--泛型程式設計(二)

泛型程式設計(二)

泛型程式碼和虛擬機器

虛擬機器沒有泛型型別物件。這句話可以理解為虛擬機器不認識泛型類,它仍然只認識普通類。

(一)型別擦除

什麼是型別擦除?

無論何時定義一個泛型型別,都自動提供了一個相應的原始型別。原始型別的名字就是刪去型別引數後的泛型型別名。而編譯器在編譯過程中去掉型別變數,就叫擦除。

什麼是補償?

編譯器在將泛型類編譯成class檔案的過程中,擦除了型別變數,但是並用限定型別來替換原始型別的過程就叫補償。如果沒有限定的變數就用Object。

為什麼要有擦除?

因為泛型在實現的早期版本中,是沒有泛型的,Java虛擬機器也不認識泛型,Java在jdk1.5後,才加入泛型,這時要讓Java虛擬機器再認識泛型,需要修改很多程式碼,也考慮到類載入器的相容性,所以放棄了。也就有了擦除,所以也有人說Java泛型是偽泛型。

比如Pair類,因為T是沒有限定的變數,所以用Object替換,我們來看看擦除後的程式碼:

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;}
}

如果是有限定的型別變數呢?原始型別用第一個限定的型別變數來替換,如果沒有就用Object來替換。

例如:

public class Inteval<T extends Comparable&Serializable> implements Serializable
{
    private T lower;
    private T upper;

    public Inteval(T first,T second)
    {
        if(firsr.compareTo(second)<=0){lower = first;upper=second;}
        else{lower = second;upper=first;
    }
}

//進行擦除和補償後,原始型別如下:
public class Inteval implements Serializable
{
    private Comparable lower;
    private Comparable upper;
    
    public Inteval(Comparable first,Comparable second)
    {
        if(firsr.compareTo(second)<=0){lower = first;upper=second;}
        else{lower = second;upper=first;
    }
}

(二)翻譯泛型表示式

我們說過Java虛擬機器不認識泛型程式碼。也就是說當我們呼叫泛型方法或域時,它都要對程式碼進行翻譯。其實這個過程中,編譯器做了兩件事:

  • 對原始方法Pair.getFirst的呼叫
  • 將返回的Object型別強制轉換String型別

我們用程式碼來看看編譯器做的事情:

Pair<Employee> pe = ....;

Employee buddy = pe.getFirst();

//事實上,我們沒有泛型時,需要進行強制型別轉換

Employee buddy = (Employee)pe.getFirst();//編譯器幫我們做了

(三)翻譯泛型方法

型別擦除也會出現在泛型方法中,我們看下面一個簡單的例子:

public static <T extends Comparable> T min(T[] a);

//進行擦除後

public static Comparable min(Comparable[] a);

但是方法擦除後,也帶來了問題。看看這個例子:

class DateInteval extends Pair<LocalDate>{
    public void setSecond(LocalDate second){
        if(second.compareTo(getFirst())>=0){
            super.setSecond(second);
        }
    }
    ...
}

當編譯器擦除後,我們再看看DateInteval類和Pair類是什麼樣的?

//擦除後的Pair類
class Pair{
    ...
    public void setSecond(Object second){
        ...
    }
}

//擦除後的DateInteval類
class DateInteval extends Pair{
    ...
    public void setSecond(LocalDate second){
        ...
    }
}

我們看到,Pair類和DateInteval類都有一個setSecond方法,但是引數型別不一樣,證明它是不同的方法。再看看呼叫時:

 DateInteval inteval = new DateInteval(...);

 Pair p = inteval;

 p.setSecond(aDate);

我們看到p.setSecond(aDate);這段程式碼找到的是setSecond(Object)方法,但是p引用的是DateInteval的物件,所以應該呼叫的是setSecond(LocalDate)方法。這時,在擦除和多型時,就發生了衝突。要解決這個問題,就需要編譯器在DateInteval類生成一個橋方法(bridge method)。

public void setSecond(Object second){
    setSecond((Date) second);
}

變數pair已經宣告為型別Pair<LocalDate>,並且這個型別只有一個簡單方法叫setSecond,即setSecond(Object);虛擬機器呼叫pair引用的物件是DateInteval型別,因而會呼叫DateInteval.setSecond(Object)方法,這個方法是合成的橋方法,它能呼叫DateInteval.setSecond(Date)方法,這正是我們想要的。

綜上所述,我們需要記住有關Java泛型轉換的事實:

①虛擬機器中沒有泛型,只有普通的類和方法

②所有的型別引數都用它們的限定型別進行替換,如果沒有限定,那就是Object

③橋方法是被合成來保持多型的

④為保持型別安全性,必要時插入強制型別轉換

約束與侷限性

(一)不能用基本型別例項化型別引數

不能用型別引數代替基本型別。因此,沒有Pair<double>,只有Pair<Double>。其原因是型別擦除。

(二)執行時型別查詢只適用於原始型別

虛擬機器中沒有泛型,它只認識原始型別。所以使用instanceof關鍵字查詢物件是否屬於某個泛型型別會得到一個編譯器錯誤。

if(a instanceof Pair<String>)// Error

if(a instanceof Pair<T>) //Error

if(a instanceof Pair)// Ok

同樣的道理,getClass方法總是返回原始型別。

Pair<String> stringPair = ...;

Pair<Employee> employeePair = ...;

if(stringPair.getClass()==employeePair.getClass())//they are equal

(三)不能建立引數化型別的陣列

不能例項化引數化型別的陣列,例如:

Pair<String>[] table = new Pair<String>[10];//Error

因為擦除後,對於陣列table而言,它的型別是Pair[],可以把它轉換成Object[],陣列會記住他的元素型別,如果再往其中儲存String型別,它會丟擲ArrayStoreException。

對於泛型型別,擦除會使這種機制無效:

objarray[0] = new Pair<Employee>();

能夠通過陣列儲存檢查,但仍會導致一個錯誤,所以不允許建立引數化型別陣列。

(四)Varargs警告

當我們建立一個引數可變的方法時:

public static <T> void addAll(Collection<T> coll,T...ts)
{
    for(t:ts) coll.add(t);
}

我們知道ts實際上是一個數組,包含提供的所有實參。那麼如果我們的ts新增的是泛型型別如Pair<String>,為了呼叫這個方法,虛擬機器就會將所有的引數t建立一個數組,也就是含有型別引數的陣列,這樣就違反了上一個規則。當然,對於這樣的呼叫,規則有所放鬆,它不是錯誤,而是警告。

可以採用兩種辦法來抑制這個警告,使用註解:

①使用@SuppressWarnings("unchecked")標註addAll方法

②在JavaSE7中,可以用@SafeVarargs標註addAll方法

(五)不能例項化型別變數

就是不能使用像new T(...),new T[...]或T.class這樣的表示式中的型別變數。

(六)不能構造泛型陣列

就像不能例項化型別變數一樣,也不能構造這樣的陣列:

T[] mm = new T[10];

(七)泛型類的靜態上下文中型別變數無效

不能在靜態域或方法中引用型別變數,例如:

public class Singleton<T>{
    //禁止使用帶有型別變數的靜態域和方法
    private static T singleton;//Error

    public static T getSingleton(){  //Error
        if(singleton==null){
            construct new instance of T
        }
        return singleton;
    }
}

(八)不能丟擲或捕獲的例項

既不能丟擲也不能捕獲泛型類物件。事實上,泛型類擴充套件Throwable都是不合法的。就是說我們不能在catch子句中使用型別變數。不過在異常規範中使用型別變數時可以的。

try
{
    ...
}
catch (T t)   //這是錯誤的,不能捕獲泛型類物件
{
    ...
}



//在異常規範中使用型別變數時允許的
public static <T extends Throwable> void doword(T t) throws T //Ok

(九)可以消除對受查異常的檢查

通過使用泛型類、擦除和@SuppressWarning註解,就能消除Java型別系統的部分基本限制,如可以消除對受查異常的檢查。

(十)注意擦除後的衝突

泛型規範說明還提到另一個原則:要想支援擦除的轉換,就需要強行限制一個類或型別變數不能同時成為兩個介面型別的子類,而這兩個介面是同一介面的不同引數化。

class Employee implements Comprable<Employee>{...}

class Manager extends Employee implements Comparable<Manager>{...}

Manager會實現Comparable<Employee>和Comparable<Manager>,這就是同一介面的不同引數化。

泛型型別的繼承規則

如Employee類和Manager類有繼承關係,但是Pair<Employee>和Pair<Manager>是沒有任何關係的。下面的程式碼將不能編譯成功:

Manager[] ms = ...;
Pair<Employee> pe = ArrayAlg.minman(ms);//Error  這是不能編譯通過的

萬用字元型別

(一)萬用字元概念

萬用字元就是用"?"代替,萬用字元型別中,允許型別引數變化。例如:

Pair<? extends Employee>

表示任何泛型Pair型別,它的型別引數是Employee的子類,如Pair<Manager>,但不是Pair<String>。因為String型別不是Employee的子類。

(二)萬用字元的超型別限定

萬用字元限定與型別變數限定十分類似,但是,還有一個附加能力,即可以指定一個超型別限定:

Pair<? super Manager>

表示型別變數可以是Manager或者是Manager的父類。

(三)無限定萬用字元

還可以使用無限定萬用字元。如:Pair<?>;注意這是與原始Pair型別不同的。型別Pair<?>有以下方法:

? getFirst();
void setFirst(?);

getFirst()的返回值只能是Object,setFirst方法不能被呼叫,甚至都不能用Object呼叫。Pair和Pair<?>的本質不同就是:可以用任意Object物件呼叫原始Pair類的setObject方法。

(四)萬用字元捕獲

編寫一個交換元素的方法:

public static void swap(Pair<?> p)

在呼叫這個方法的時候,萬用字元不是型別變數,所以"?"不能作為一種型別,也就是說下面的程式碼是錯誤的:

? t = p.getFirst();//Error
p.setFirst(p.getSecond());
p.setSecond(t);

這樣就出現了問題,那麼我們如何解決這個問題呢?我們提供了一個輔助方法:

public static <T> void swapHelper(Pair<T> p)
{
    T t = p.getFirst();
    p.setFirst(p.getSecond());
    p.setSecond(t);
}

那麼如果想要呼叫swap方法,可以通過swapHelper方法:

public static void swap(Pair<?> p)
{
    swapHelper(p);
}

這個就類似於橋方法,這樣我們就將“萬用字元”捕獲了,在這種情況下,萬用字元捕獲是不可避免的。