1. 程式人生 > >Java核心技術-泛型程序設計

Java核心技術-泛型程序設計

anti get 6.4 checked throw bsp 但是 通配符 們的

使用泛型機制編寫的代碼要比那些雜亂地使用Object變量,然後再進行強制類型轉換的代碼具有更好的安全性和可讀性。

泛型對於集合類尤其有用

1 為什麽要使用反省程序設計

泛型程序設計意味著編寫的代碼可以被很多不同類型的對象所重用。

1.1 類型參數的好處

在Java中增加泛型類之前,泛型程序設計是用繼承實現的。ArrayList類只維護一個Object引用的數組:

private class ArraryList
{
    private Object[] elementData;
    ...
    public Object get(int i){...}
    public void
add(Object o){...} }

這種方法有兩個問題:

1.當獲取一個值的時候必須進行強制類型轉換

2.沒有錯誤檢查,可以向數組列表中添加任何類的對象

泛型提供了一個更好的解決方案:類型參數,ArrayList類有一個類型參數用來指示元素的類型:

ArrayList<String> files=new ArrayList<String>();

*這使得代碼具有更好的可讀性

Java SE 7及以後的版本中可以在構造函數中省略泛型類型:

ArrayList<String> files=new ArrayList<>();

省略的類型可以從變量的類型

推斷得出。

*編譯器也可以很好的利用這個信息,當獲取一個值時不需要進行強制類型轉換,編譯器就知道返回值類型為String。

*編譯器還知道ArrayList<String>中add方法有一個類型為String的參數,並且編譯器會進行檢查,避免插入錯誤類型的對象

類型參數的魅力在於:使得程序具有更好的可讀性和安全性。

1.2 誰想稱為泛型程序員

使用一個像ArrayList的泛型類很容易,但是實現一個泛型類並沒有那麽容易。

例如,ArrayList類有一個addAll方法用來添加另一個集合的全部元素。可以將ArrayList<Manage>中的所有元素添加到ArrayList<Employee>中去,但是反過來就不行。如果只能允許前一個調用,而不能允許後一個調用呢?(通配符類型)


2 定義簡單泛型類

一個泛型類就是具有一個或多個類型變量的類。

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 T getSecond(){return second;}
    
    public void setFirst(T newValue){ first=newValue;}
    public void setSecond(T newValue){second=newValue;}
}

這裏引入了一個類型變量T,用尖括號(<>)括起來,並放在類名的後面。

泛型類可以有多個類型變量,例如:

public class Pair<T,U>

類定義中的類型變量指定方法的返回類型以及域和局部變量的類型。

用具體的類型替換類型變量就可以實例化泛型類型,可以將結果想象成帶有構造器的普通類,換句話說,泛型類可看作普通類的工廠


3 泛型方法

定義一個帶有類型參數的簡單方法:

class ArrayAlg
{
    public static <T> T getMiddle(T ...a)
    {
        return a[a.length/2];
    }
}

類型變量放在修飾符的後面,返回類型的前面。

泛型方法可以定義在普通類中,也可以定義在泛型類中。


4 類型變量的限定

有時,類或方法需要對類型變量加以約束:

class ArrayAlg
{
    public static <T> T min(T[] a)
    {
        if(a==null || a.length==0) return null;
        T smallest=a[0];
        for(int i=1;i<a.length;i++)
            if(smallest.compareTo(a[i])>0) smallest=a[i];
        return smallest;
    }
}

這裏需要將T限制為實現了Comparable接口的類。可以通過類型變量T設置限定實現這點:

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

一個類型變量或通配符可以有多個限定(可以有多個接口超類型,但至多有一個類):

T extends Comparable & Serializable

限定類型用“&”分隔,用逗號來分隔類型變量,如果用一個類作為限定,它必須是限定列表中的第一個。

5 泛型代碼和虛擬機

虛擬機沒有泛型類型對象——所有對象都屬於普通類。

5.1 類型擦除

無論何時定義一個泛型類型,都自動提供了一個相應的原始類型。原始類型的名字就是刪去類型參數後的泛型類型名。

擦除類型變量,並替換為限定類型(無限定的變量用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替換。

假定有一個泛型類Interval:

public class Interval<T extends Compareble & Serializable> 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){...}
}

註:如果切換限定:class Interval<T extends Serializable & Comparable>,則原始類型用Serializable替換T,而編譯器在必要時要插入Comparable強制類型轉換。

為了提高效率,應該將標簽接口(沒有方法的接口)放在邊界列表的末尾。

5.2 翻譯泛型表達式

當程序調用泛型方法時,如果擦除返回類型,編譯器插入強制類型轉換,即:

1.對原始方法Pair.getFirst的調用

2.將返回的Object類型強制轉換為Employee類型

當存取一個泛型域時也要插入強制類型轉換

5.3 翻譯泛型方法

方法擦除帶來了兩個復雜的問題:

有一個日期區間是一對LocalDate對象,並且需要覆蓋Pair中的setSecond這個方法來確保第二個值永遠不小於第一個值。

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

但是類型擦除後變成:

class DateInterval extends Pair
{
    public void setSecond(LocalDate second){...}
    ...
}

發現除了原先覆蓋超類中的setSecond(LocalDate second),此時又多了一個setSecond(Object second)方法,由於方法參數不同,所以這兩個顯然是不同的方法。

考慮下面的調用:

DateInterval interval=new DateInterval(...);
Pair<LocalDate> pair=interval;
pair.setSecond(aDate);

這裏,希望對setSecond的調用具有多態性,並調用最合適的方法。但由於類型擦除與多態發生了沖突。要解決這個問題需要在DateInterval中生成一個橋方法(再覆蓋超類的setSecond(Object second)方法)

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

假設DateInterval也覆蓋了超類的getSecond方法:

class DateInterval extends Pair<LocalDate>
{
    public LocalDate getSecond(){
        return (Date)super.getSecond().clone(); 
    }
}

由於類型擦除,此時DateInterval中有兩個getSecond方法:

LocalDate getSecond()

Object getSecond()

顯然,在Java中,具有相同參數類型的兩個同名方法是不合法的,但是,在虛擬機中,用參數類型和返回類型確定一個方法。因此,編譯器可能產生兩個僅返回類型不同的方法字節碼,但虛擬機能夠正確處理這一情況。我們稱這種情況為這兩種方法具有協變的返回類型

Java泛型轉換的事實:

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

*所有的類型參數都用它們的限定類型替換

*橋方法被用來保持多態

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

5.4 調用遺留代碼

設計Java泛型類型時,主要目標是允許泛型代碼和遺留代碼之間能夠相互操作。

可以使用@SuppressWarnings("unchecked")消除警告


6 約束與局限性

下面將討論使用Java泛型時的一些限制,大多數限制是由類型擦除引起的。

6.1 不能用基本類型實例化類型參數

可以使用基本類型的包裝類,如Pair<Double>,擦除之後Pair類可以含Object類型的域,而Object不能存儲double。

6.2 運行時類型查詢只適用於原始類型

例如:

if(a instanceof Pair<String>

事實上僅僅測試a是否是任意類型的一個Pair

同樣:

Pair<String> stringPair=...;
Pair<Employee> employeePair=...;
if(stringPair.getClass()==employee.getClass())  //they are equal

兩次調用getClass(),返回的都是Pair.class

6.3 不能創建參數化類型的數組

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

因為類型擦除後允許:

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

如果需要收集參數化類型對象,只有一種安全而有效的方法:使用ArrayList:ArrayList<Pair<String>>

6.4 Varargs警告

向參數個數可變的方法傳遞一個泛型類型的實例:

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

為調用這個方法,虛擬機必須建立一個Pair<String>數組,這就違反了前面的規則,不過,對於這種情況有所放松,只會得到一個警告,而不是錯誤。

可以使用註解@SuppressWarnings("unchecked")或@SafeVarargs抑制這個警告。

6.5 不能實例化類型變量

不能使用像new T(...)、 new T[...]或T.class這樣的表達式中的類型變量:

public Pair(){ first=new T(); second=new T(); } //Error

最好的解決辦法是讓調用者提供一個構造器表達式:

Pair<String> p=Pair.makePair(String::new);

makePair方法接收一個Supplier<T>,這是一個函數式接口,表示一個無參數而且返回類型為T的函數:

public static <T> Pair<T> makePair(Supplier<T> constr)
{
    return new Pair<>(constr.get(),constr.get());
}

constr.get()方法是Supplier<T>函數執行構造器引用的方法。

比較傳統的方法是使用反射來構造泛型對象,不能調用:

first=T.class.newInstance(); //Error

T.class會被擦除成Object.class

必須像下面這樣設計API以便得到一個Class對象:

public static <T> Pair<T> makePair(Class<T> c1)
{
    try{ return new Pair<>(c1.newInstance(),c1.newInstance());}
    catch(Exception ex) { return null; }
}

然後這樣調用:

Pair<String> p=Pair.makePair(String.class)

6.6 不能構造泛型數組

如同不能構造泛型實例一樣,也不能實例化數組。

最好讓用戶提供一個數組構造器表達式:

String[] ss=ArrayAlg.minmax(String[]::new,"Tom","Dick","Harry");

利用數組構造器表達式實現:

public static <T extends Comparable> T[] 
    minmax(IntFunction<T[]> constr,T... a)
{
    T[] mm=constr.apply(2);
}

利用反射實現:

public static <T extends Comparable> T[]  minmax(T... a)
{
    T[] mm=Array.newInstance(a.getClass().getComponentType(),2);
}

6.7 泛型類的靜態上下文中類型變量無效

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

private static T singleInstance;

public static T getSingleInstance(){...}

6.8 不能拋出或捕獲泛型類的實例

泛型類擴展Throwable都是不合法的:

public class Problem<T> extends Exception{...] //Error

catch子句中不能使用類型變量:

catch(T e) //Error

6.9 可以消除對受查異常的檢查

Java異常處理的一個基本原則是,必須為所有受查異常提供一個處理器。不過可以利用泛型消除這個限制。關鍵在於以下方法:

@SuppressWarning("unchecked")
public static <T extends Throwable> void 
        throwsAs(Throwable e)throws T
{
    throw (T) e;
}    

6.10 註意擦出後的沖突

有這樣一個泛型類:

public class Pair<T>
{
    public boolean equals(T valule) 
    {
        return first.equals(value)&&second.equals(value);
    }
}

考慮這樣一個問題:

Pair<T>泛型擦除後有兩個equals方法:

boolean equals(Object) //define in Pair<T>

boolean equals(Object)//inherited from Object

補救的方法是重命名引發錯誤的方法。

要想支持擦除的轉換,就需要強行限制一個類或類型變量不能同時成為兩個接口類型的子類,而這兩個接口是同一接口的不同參數化。

例如,下述代碼是非法的:

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

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

Manager會實現Comparable<Employee>和Comparable<Manager>,這是同一接口的不同參數化。

其原因非常微妙,有可能與合成橋方法產生沖突。

7 泛型類型的繼承規則

在使用泛型時,需要了解一些有關繼承和子類型的準則。

如Manager是Employee的子類,那麽Pair<Manager>是Pair<Employee>的一個子類嗎? 答案是:“不是”

規則:無論S與T有什麽聯系。通常,Pair<S>與Pair<T>沒有什麽聯系。

技術分享圖片


8 通配符類型

固定的泛型類型系統使用起來並沒有那麽令人愉快,Java設計者發明了一種巧妙的“解決方案”:通配符類型。

泛型通配符(?)和類型變量(T)的區別——任何泛型類型vs某一種泛型類型

?可以在聲明變量時使用,例如:

Pair<Manager> managerBuddies=new Pair<>(ceo,cfo);

Pair<? extends Employee> wildcardBuddies=managerBuddies;

8.1 通配符概念

通配符類型中,允許類型參數變化。

例如,通配符類型:

Pair<? extends Employee>

表示任何泛型Pair類型,它的類型參數是Employee的子類,如Pair<Manager>,但不是Pair<String>

正如前面說到的問題:不能將Pair<Manager>傳遞給Pair<Employee>,解決方法是:使用通配符類型。

技術分享圖片

Pair<? extends Employee>中的方法似乎是這樣:

? extends Employee getFirst()

void setFirst(? extends Employee)

這樣將不能調用setFirst方法,編譯器只知道需要某個Employee的子類型,但不知道具體是什麽類型,所以拒絕傳遞任何特定的類型。

使用getFirst就不存在這個問題:將getFirst的返回值賦給一個Employee的引用完全合法

這就是引入有限定的通配符的關鍵之處。

8.2 通配符的超類型限定

通配符限定與類型變量限定十分類似,但是,還有一個附加的能力,即可以指定一個超類型限定:

? super Manager

這個通配符限制為Manager的所有超類型

引入super的原因:

帶有超類型限定的通配符的行為之前介紹的的Pair<? extends Employee>行為恰恰相反,可以為方法提供參數,但不能使用返回值

直觀來說,帶有超類型限定的通配符可以向泛型對象寫入,帶有子類型限定的通配符可以從泛型對象讀取。

技術分享圖片

8.3 無限定通配符

還可以使用無限定的通配符,如:Pair<?>,它有以下方法:

? getFirst()

void setFirst()

getFirst的返回值只能賦給一個Object。setFirst方法不能被調用

8.4 通配符捕獲

通配符不是類型變量,因此不能在編寫代碼時使用“?”作為一種類型,如:

? t=p.getFirst();

如何臨時保存一個通配符類型變量,需要一個輔助方法,如下:

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

這裏使用swapHelper方法的參數T捕獲通配符。

通配符捕獲只有在有許多限制的情況下是合法的,編譯器必須確定通配符表達的是單個、確定的類型。

如:ArrayList<Pair<?>>中的通配符就不能通過ArrayList<Pair<T>>捕獲。數組列表可以保存兩個Pair<?>,分別針對?的不同類型。


9 反射和泛型

反射允許你在運行時分析任意對象。但如果對象是泛型類的實例,關於泛型類型參數則得不到太多信息,因為它們會被擦除。

下面將了解利用反射可以獲得泛型類的什麽信息。

9.1 泛型Class類

現在,Class類是泛型的,類型參數十分有用,這是因為它允許Class<T>方法的返回類型更加具有針對性

Class<T>中的方法就使用了類型參數:

T newInstance()——返回一個實例,這個實例所屬的類由默認的構造器獲得。

T cast(Object obj)——如果給定的類型確實是T的一個子類型,cast方法就會返回一個現在聲明為類型T的對象

T[] getEnumConstants()——如果這個類不是enum類或類型T的枚舉值的數組,返回null

Class<? super T>get SuperClass()

Constructor<T> getConstructor(Class... parameterTypes)

Constructor<T> getDeclaredConstructor(Class... parameterTypes)

9.2 使用Class<T>參數進行類型匹配

有時,匹配泛型方法中的Class<T>參數的類型變量很有實用價值:

public static <T> makePair(Class<T> c)
                                          throws InstantiationException         
{
       return new Pair<>(c.newInstance(),c.newInstance());
}

如果調用makePair(Employee.class),makePair方法的類型參數T同Employee匹配,並且編譯器可以推斷出這個方法將返回一個Pair<Employee>

9.3 虛擬機中的泛型類型信息

Java泛型的卓越特性之一是在虛擬機中泛型類型的擦除。令人感到奇怪的是,擦除的類仍然保留一些泛型祖先的微弱記憶。

例如,原始的Pair類知道源於泛型類Pair<T>,但不知道具體的類型變量。

類似地,方法:

public static Comparable min(Comparable[] a)

這是一個泛型方法的擦除:

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

可以使用反射API來確定:

*這個泛型方法有一個叫做T的類型參數

*這個類型參數有一個子類型限定,其自身又是一個泛型類型

*這個限定類型有一個通配符參數

*這個通配符參數有一個超類型限定

*這個泛型方法有一個泛型數組參數

可以重新構造實現者聲明的泛型類以及方法中的所有內容。但是,不會知道對於特定的對象或方法調用,如果解釋類型參數。

為了表達泛型類型聲明,使用java.lang.reflect包中提供的接口Type,這個接口包含下列子類型:

*Class類,描述具體類型

*TypeVariable接口,描述類型變量(如T extends Comparable<? super T>)

*WildcardType接口,描述通配符(如? super T)

*ParameterizedType接口,描述泛型類或接口類型(如Comparable<? super T>)

*GenericArrayType接口,描述泛型數組(如T[])

Java核心技術-泛型程序設計