1. 程式人生 > 實用技巧 >CORE JAVA 第八章 泛型程式設計

CORE JAVA 第八章 泛型程式設計

第八章 泛型程式設計

8.1 為什麼要使用泛型程式設計

​ 泛型程式設計意味著編寫的程式碼可以被很多不同型別的物件所重用。

8.1.1 型別引數的好處

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

public class ArrayList	// before generic classes
{
    private Object[] elementData;
    ……
    public Object get(int i){……}
    public void add(Object o){……}
}

​ 這種方法有兩個問題:當獲取一個值的時候必須進行強制型別轉換;沒有錯誤檢查,可以向陣列列表中新增任何類的物件。

​ 對於這些呼叫,編譯和執行都不會出錯。然而在其他地方,如果將get的結果強制型別轉換為String型別,就會產生一個錯誤。

​ 泛型提供了一個更好的解決方案:型別引數。型別引數用來指示元素的型別:

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

註釋:在Java SE 7及以後的版本中,建構函式中可以省略泛型型別:

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

​ 編譯器可知道型別引數的型別。呼叫get方法時不需要進行強制型別轉換;呼叫add方法時可以檢查避免插入錯誤型別的物件。

​ 型別引數的魅力在於:使得程式具有更好的可讀性和安全性。

8.1.2 誰想成為泛型程式設計師

​ 一個泛型程式設計師的任務就是預測出所用類的未來可能有的所有用途。

萬用字元型別??

8.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 void setFirst(T newValue) { first = newValue; }
}

​ Pair類引入了一個型別變數T,用尖括號括起來,並放在類名的後面。

​ 泛型類可以有多個型別變數。

public class Pair<T, U> {……}

其中第一個域和第二個域使用不同的型別。

​ 類定義中的型別變數指定方法的返回型別以及域和區域性變數的型別。

註釋:在Java庫中,使用E表示集合的元素型別,K和V分別表示表的關鍵字與值的型別。T/U/S表示“任意型別”。

​ 用具體的型別替換型別變數就可以例項化泛型型別。

8.3 泛型方法

​ 定義一個帶有型別引數的簡單方法。

class ArrayAlg
{
    public static <T> T getMiddle(T... a)
        ……
}

​ 這個方法是在普通類中定義的,而不是在泛型類中定義的。注意,型別變數放在修飾符的後面,返回型別的前面。

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

​ 當呼叫一個泛型方法時,在方法名前的尖括號中放入具體的型別:

String middle = ArrayAlg.<String>getMiddle("John", "Q.", "Public");

​ 在這種情況(實際也是大多數情況)下,方法呼叫可以省略型別引數。編譯器有足夠的資訊能夠推斷出所呼叫的方法。它用names的型別(即String[])與泛型型別T[]進行匹配並推斷出T一定是String。也就是說,可以呼叫

String middle = ArrayAlg.getMiddle("John", "Q.", "Public");

​ 幾乎在大多數情況下,對於泛型方法的型別引用沒有問題。偶爾,編譯器也會提示錯誤,此時需要解譯錯誤報告。例:

double middle = ArrayAlg.getMiddle(3.14, 1729, 0);

​ 編譯器將會自動打包引數為1個Double和2個Integer物件,而後尋找這些類的共同超型別。

8.4 型別變數的限定

​ 有時,類或方法需要對型別變數加以約束。

​ 因為型別變數T可以是任何類的物件,若想確信型別變數T所屬的類有compareTo方法,可以將型別變數T限制為實現了Comparable介面的類。可以通過對型別變數T設定限定(bound)實現這一點:

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

​ 實際上Comparable介面本身就是一個泛型型別。

​ 現在,沒有實現Comparable介面的類呼叫min方法將會產生一個編譯錯誤。

​ 在此為什麼使用extends而不是implements?畢竟Comparable是一個介面。

​ 下面的記法

<T extends BoundingType>

表示T應該是繫結型別的子型別(subtype)。T和繫結型別可以是類,也可以是介面。

​ 一個型別變數或萬用字元可以有多個限定,例如:

T extends Comparable & Serializable

​ 限定型別用&分隔,而逗號用來分隔型別變數。

​ 在Java的繼承中,可以根據需要擁有多個介面超型別,但限定中至多有一個類。如果用一個類作為限定,它必須是限定列表中的第一個。

8.5 泛型程式碼和虛擬機器

​ 虛擬機器沒有泛型型別物件——所有物件都屬於普通類。

8.5.1 型別擦除

​ 泛型是 Java 1.5 版本才引進的概念,在這之前是沒有泛型的概念的,但顯然,泛型程式碼能夠很好地和之前版本的程式碼很好地相容。因為,泛型資訊只存在於程式碼編譯階段,在進入 JVM 之前,與泛型相關的資訊會被擦除掉,專業術語叫做型別擦除。

​ 無論何時定義一個泛型型別,都自動提供了一個相應的原始型別。原始型別的名字就是刪去型別引數後的泛型型別名。擦除型別變數,並替換為限定型別(無限定的變數用Object)。

​ 例如,Pair的原始型別如下所示:

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 newValue) { first = newValue; }
}

​ 結果是一個普通的類,就好像泛型引入Java語言之前已經實現的那樣。

​ 在程式中可以包含不同型別的Pair,例如,Pair或Pair。而擦除型別後就變成原始的Pair型別了。

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

8.5.2 翻譯泛型表示式

​ 當程式呼叫泛型方法時,如果擦除返回型別,編譯器插入強制型別轉換。例如:

Pair<Employee> buddies = ……;
Employee buddy = buddies.getFirst();

​ 擦除getFirst的返回型別後將返回Object型別。編譯器自動插入Employee的強制型別轉換。也就是說,編譯器把這個方法呼叫翻譯為兩條虛擬機器指令:

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

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

8.5.3 翻譯泛型方法

​ 型別擦除也會出現在泛型方法中。程式設計師通常認為下述的泛型方法

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

是一個完整的方法族,而擦除型別之後,只剩下一個方法:

public static Comparable min(Comparble[] a)

​ 因為 java 在編譯原始碼時, 會進行 型別擦除, 導致泛型型別被替換限定型別(無限定型別就使用 Object). 因此為保持繼承和過載的多型特性, 編譯器會生成 橋方法.

Pair 是個泛型類, 它具有泛型方法 setSecond(T second),
在經過編譯時的 型別擦除 後變為 setSecond(Object second).

DateIntervalPair<LocalDate> 的例項化子類, 它具有方法 setSecond(LocalDate second).

​ 我們新建 DateInterval 物件, 並用基類 Pair<LocalDate> 來引用它,此時呼叫基類的 setSecond 方法時, 我們希望它能夠實現多型, 即呼叫 DateInterval.setSecond(LocalDate) 方法.
​ 事實上, java 編譯器通過插入 橋方法 的方式, 幫助我們實現了該功能(解決了型別擦除與多型發生的衝突:因為有了型別擦除,不滿足多型的條件:繼承/重寫).

​ 反編譯 DateInterval.class 會發現它具有兩個 setSecond 方法:

  1. void setSecond(LocalDate);

  2. void setSecond(Object).

    並且, 虛擬機器用pair引用的物件呼叫 void setSecond(Object) 中會呼叫 DateInteval.setSecond(Object), 這個方法是合成的橋方法.它呼叫DateInteval.setSecond(Date)。這正是我們期望的操作效果。

​ 另外,DateIntervalPair<LocalDate> 的例項化子類,不能這樣編寫程式碼:

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

​ 在DateInterval類中,有兩個getSecond方法:

LocalDate getSecond() 	// defined in DateInterval
Object getSecond()	// overrides the method defined in Pair to call the first method

​ 在這裡,具有相同引數型別的兩個方法是不合法的。它們都沒有引數。但是,在虛擬機器中,用引數型別和返回型別確定一個方法。因此,編譯器可能產生兩個僅返回型別不同的方法位元組碼,虛擬機器能夠正確地處理這樣情況。

註釋:橋方法不僅用於泛型型別。

在一個方法覆蓋另一個方法時可以指定一個更嚴格的返回型別:

public class Employee implements Cloneable
{
    public Employee clone() throws CloneNotSupportedException {……}
}

Object.clone和Employee.clone方法被說成具有協變的返回型別。

實際上,Employee類有兩個克隆方法:

Employee clone()	//defined above
Object clone()

合成的橋方法呼叫了新定義的方法。

​ 有關Java泛型轉換的事實:

  • 虛擬機器中沒有泛型,只有普通的類和方法
  • 所有的型別引數都用它們的限定型別替換
  • 橋方法被合成來保持多型
  • 為保持型別安全性,必要時插入強制型別轉換

8.5.4 呼叫遺留程式碼

​ 設計Java泛型型別時,主要目標是允許泛型程式碼和遺留程式碼之間能夠互操作。

​ 當將一個泛型物件傳遞給一個引數是原始型別的函式時,編譯器會發出一個警告。

Dictionary<Integer, Component> labelTable = new Hashtable<>();
slider.setLabelTable(labelTable);	// Warning
// void setLabelTable(Dictionary table)

編譯器無法確定這個方法會對這個泛型物件做什麼操作,未來的操作有可能會產生強制型別轉換的異常。

​ 另一個相反的情形是由一個遺留的類得到一個原始型別的物件。可以將它賦給一個引數化的型別變數,當然,這樣做會看到一個警告。例如:

Dictionary<Integer, Components> labelTable = slider.getLabelTable();	// Warning

​ 最差的情況就是程式丟擲一個異常。

​ 在查看了警告之後,可以利用註解使之消失。註釋必須放在生成這個警告的程式碼所在的方法之前:

@SuppressWarning("unchecked")
Dictionary<Integer, Components> labelTable = slider.getLabelTable();	// No warning

​ 或者,可以標註整個方法,如下:

@SuppressWarning("unchecked")
public void configureSlider()	{……}

​ 這個註解會關閉對方法中所有程式碼的檢查。

8.6 約束與侷限性

​ 下面將闡述使用Java泛型時需要考慮的一些限制。大多數限制都是由型別擦除引起的。

8.6.1 不能用基本型別例項化型別引數

​ 不能用型別引數代替基本型別。因此,沒有Pair<double>,只有Pair<Double>

​ 其原因是型別擦除。擦除之後,Pair類含有Object型別的域,而Object不能儲存double值。

8.6.2 執行時型別查詢只適用於原始型別

​ 虛擬機器中的物件總有一個特定的非泛型型別。因此,所有的型別查詢只產生原始型別。例如:

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

實際上僅僅測試a是否是任意型別的一個Pair。下面的測試同樣如此:

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

或強制型別轉換:

Pair<String> p = (Pair<String>) a;	//Waring——can only test that a is a Pair

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

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

其比較的結果是true,這是因為兩次呼叫getClass都將返回Pair.class。

8.6.3 不能建立引數化型別的陣列

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

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

​ 擦除後,table的型別是Pair[]。如果把它轉換為Object[]

Object[] objarray = table;

​ 並試圖儲存其他型別的元素,就會丟擲一個異常:

objarray[0] = "Hello"; // Error——component type is Pair

​ 不過對於泛型型別,擦除會使這種機制無效。以下賦值:

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

能夠通過陣列儲存檢查,不過仍會導致一個型別錯誤。出於這個原因,不允許建立引數化型別的陣列。

​ 需要說明的是,只是不允許建立這些陣列,而宣告型別為Pair<String>[]的變數仍是合法的。不過不能用new Pair<String>[10]初始化這個變數。

提示:如果需要收集引數化型別物件,只有一種安全而有效的方法:使用ArrayList:

ArrayList<Pair<String>>

8.6.4 Varargs警告

​ 向引數個數可變的方法傳遞一個泛型型別的例項。

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

​ 實際上引數ts是一個數組,包含提供的所有實參。

​ 現在考慮以下呼叫:

Collection<Pair<String>> table = ……;
Pair<String> pair1 = ……;
Pair<String> pair2 = ……;
addAll(table, pair1, pair2);

​ 為了呼叫這個方法,Java虛擬機器必須建立一個Pair<String>陣列,這就違反了前面的規則。不過,對於這種情況,規則有所放鬆,你只會得到一個警告而不是錯誤。

​ 可以採用兩種方法來抑制這個警告。一種方法是為包含addAll呼叫的方法增加註解@SuppressWarnings("unchecked")。或者在Java SE 7中,還可以用@SafeVarargs直接標註addAll方法。

註釋:可以使用@SafeVarargs標註來消除建立泛型陣列的有關限制:

@SafeVarargs static <E> E[] array(E... array)	{return array;}

​ 現在可以呼叫:

Pair<String>[] table = array(pair1, pair2);

​ 以下程式碼:

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

能順利執行而不會出現ArrayStoreException異常,但在處理table[0]時會在別處得到一個異常。

8.6.5 不能例項化型別變數

​ 不能使用像new T(……)new T[……]T.class這樣的表示式中的型別變數。型別擦除將T改變成Object。

​ 例如,下面的構造器就是非法的:

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

兩種方式解決如何例項化型別變數

​ 在Java SE 8之後,最好的解決方法是讓呼叫者提供一個構造器表示式:

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

​ 比較傳統的解決方法是通過反射呼叫Class.newInstance方法構造泛型物件。

​ 不能呼叫

first = T.class.newInstance();
// T.class會被擦除為Object.class

​ 應像這樣設計以便得到一個Class物件:

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

這個方法可以按照下列方式呼叫:

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

​ 注意,Class類本身是泛型。例如,String.class是一個Class<String>的(唯一)例項。因此,makePair方法能夠推斷出pair的型別。

8.6.6 不能構造泛型陣列

​ 陣列本身也有型別,用來監控儲存在虛擬機器中的陣列。這個型別會被擦除。例如:

public static <T extends Comparable> T[] minmax(T[] a)	{ T[] mm = new T[2]; ……}	// Error

​ 型別擦除會讓這個方法永遠構造Comparable[2]陣列。

​ 如果陣列僅僅作為一個類的私有例項域,就可以將這個陣列宣告為Object[],並且在獲取元素時進行型別轉換。

​ 例如,ArrayList類可以這樣實現:

public class ArrayList<E>
{
    private Object[] elements;
    ……
    @SuppressWarnings("unchecked")	public E get(int n)	{return (E) elements[n]; }
    public void set(int n, E e) { elements[n] = e; }	// no cast needed
}

​ 實際的實現沒有這麼清晰:

public class ArrayList<E>
{
    private E[] elements;
    ……
    public ArrayList()	{ elements = (E[]) new Object[10]; }
}

​ 這裡,強制型別轉換E[]是一個假象,而型別擦除使其無法察覺。

兩種方式解決想要例項化陣列而型別擦除引起的ClassCastException??

​ 由於minmax方法返回T[]陣列,而不是僅僅作為私有域,使得上面這一技術無法施展。如果掩蓋這個型別會有錯誤結果。

public static <T extends Comparable> T[] minmax(T... a)	
{ 
    Object[] mm = new Object[2];
    ……
    
    return (T[]) mm;
}	

​ 呼叫:

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

編譯時不會有任何警告。當Object[]引用賦給Comparable[]變數時,將會發生ClassCastException異常。

​ 在這種情況下,最好讓使用者提供一個數組構造器表示式:

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

構造器表示式指示一個函式,給定所需的長度,會構造一個指定長度的String陣列。

​ minmax方法使用這個引數生成一個有正確型別的陣列:

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

}	

​ 比較老式的方法是利用反射,呼叫Array.newInstance

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

8.6.7 泛型類的靜態上下文中型別變數無效

​ 不能在靜態域或方法中引用型別變數。

8.6.8 不能丟擲或捕獲泛型類的例項

​ 泛型類甚至都不能擴充套件Throwable。

​ catch子句中不能使用型別變數。不過,在異常規範中使用型別變數(方法頭thorws中)是允許的。

8.6.9 可以消除對受查異常的檢查?

8.6.10 注意擦除後的衝突

​ 當泛型型別被擦除時,無法建立引發衝突的條件。

​ 方法的引數被擦除,若該方法與繼承來的方法重名,而且擦除後的引數型別與繼承的方法的引數型別相同,就會發生衝突。補救的方法是重新命名引發錯誤的方法。

​ 要想支援擦除的轉換,就需要強行限制一個類或型別變數不能同時成為兩個介面型別的子類,而這兩個介面是同一介面的不同引數化。例如:

class Employee implements Comparable<Employee>	{……}
class Manager extends Employee implements Comparable<Manager>	// Error
{……}

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

​ 其原因非常微妙。有可能與合成的橋方法產生衝突。實現了Comparable<X>的類可以獲得一個橋方法:

public int compareTo(Object other) {return compareTo(X) other;}

對於不同型別的X不能有兩個這樣的方法。

8.7 泛型型別的繼承規則

ManagerEmployee的子類,但Pair<Manager>不是Pair<Employee>的子類。

​ 永遠可以將引數化型別轉換為一個原始型別。例如,Pair<Employee>是原始型別Pair 的一個子型別。轉換成原始型別後,會產生型別錯誤。例:

Pair<Manager> managerBuddies = new Pair<>(ceo, cfo);
Pair rawBuddies = managerBuddies;	// OK
rawBuddies.setFirst(new File("……"));	// only a compile-time warning

​ 這裡失去的只是泛型程式設計提供的附加安全性。

​ 泛型類可以擴充套件或事項其他的泛型類。

8.8 萬用字元型別

8.8.1 萬用字元概念

​ 萬用字元型別中,允許型別引數變化。例如

Pair<? extends Employee>

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

	#### 通過有限定的萬用字元,可以區分安全的訪問器方法和不安全的更改器方法???

8.8.2 萬用字元的超型別限定!!!

​ 可以指定一個超型別限定:

? super Manager

這個萬用字元限制為Manager的所有超型別。

​ 帶有超型別限定的萬用字元可以為方法提供引數,但不能使用返回值。例如,Pair<? super Manager>有方法void setFirst(? super Manager)? super Manager getFirst()

​ 這不是真正的Java語法,但是可以看出編譯器知道什麼。編譯器無法知道setFirst方法的具體型別,因此呼叫這個方法時不能接受型別為EmployeeObject的引數。只能傳遞Manager型別的物件,或某個子型別的物件。另外,如果呼叫getFirst,不能保證返回物件的型別。只能把它賦給一個Object

​ 直觀地講,帶有超型別限定的萬用字元可以向泛型物件寫入,帶有子型別限定的萬用字元可以從泛型物件讀取。

超型別限定的另一應用 ??

<T extends Comparable<? super T>>

​ 超型別限定的另一個常見的用法是作為一個函式式介面的引數型別。

8.8.3 無限定萬用字元

Pair<?>

​ 例如,Pair<?>有方法void setFirst(?)?getFirst()

getFirst的返回值只能賦給一個ObjectsetFirst方法不能被呼叫,甚至不能用Object呼叫。

Pair<?>Pair本質的不同在於:可以用任意Object物件呼叫原始Pair類的setObject方法。

8.8.4 萬用字元捕獲???

8.9 反射和泛型

8.9.1 泛型Class類

Class<T>中的許多方法使用了型別引數,使得Class<T>方法的返回型別更加具有針對性。

8.9.2 使用Class<T>引數進行型別匹配

​ 有時,匹配泛型方法中的Class<T>引數的型別變數很有實用價值。

8.9.3 虛擬機器中的泛型型別資訊??

​ 虛擬機器?需要重新構造實現者宣告的泛型類以及方法中的所有內容。但是,不會知道對於特定的物件或方法呼叫,如何解釋型別引數。

個人理解

​ 使用強制型別轉換,沒有型別檢測,只能夠在執行時候,系統丟擲異常後,你才會發現錯誤;

​ 泛型的特點:

  1. 能夠對型別進行限定
  2. 在編譯期對型別進行檢查,編譯時報錯
  3. 對於獲取明確的限定型別,無需進行強制型別轉化

Java中泛型是不變的,而陣列是協變的.

在java泛型中,引入了 ?(萬用字元)符號來支援協變和逆變.型別萬用字元一般是使用?代替具體的型別實參,注意了,此處’?’是型別實參,而不是型別形參 !!

萬用字元表示一種未知型別,並且對這種未知型別存在約束關係.

https://blog.csdn.net/qq_35890572/article/details/80402840

https://segmentfault.com/a/1190000005337789

? 與 T 的差別

  1. ? 表示一個未知型別, T 是表示一個確定的型別. 因此,無法使用 ?T 宣告變數和使用變數.如
    // OK
    static <T> void test1(List<T> list) {
        T t = list.get(0);
        t.toString();
    }
    // Error
    static void test2(List<?> list){
        ? t = list.get(0);
        t.toString();
    }```java
  1. ? 主要表示**使用泛型**,T`表示宣告泛型