JAVA 泛型總結(結合JAVA核心技術和Effective Java兩書)

一、基礎知識

1、類型擦除

類型擦除指的是通過類型參數合併, 將泛型類型實例關聯到同一份字節碼上。 編譯器只爲泛型類型生成一份字節碼, 並將其實例關聯到這份字節碼上, 因此泛型類型中的靜態變量是所有實例共享的。
(1) 一個static 方法, 無法訪問泛型類的類型參數, 因爲類還沒有實例化, 所以, 若static方法需要使用泛型能力, 必須使其成爲泛型方法,(泛型參數稍後會介紹),即沒有成爲泛型方法的靜態方法不能訪問類型參數。
(2) 泛型類的靜態上下文中類型變量無效,即靜態變量不能引用類型變量。

public class Singleton<T>{
 private static T singleInstance;//錯誤,違反第二條
 public static T getSingleInstance() // 錯誤,違反第一條
 {
 if(singleInstance == null)
 Return singleInstance;
 }
}

(3) 在使用泛型時, 任何具體的類型都被擦除, 唯一知道的是你在使用一個對象。 比如:List<String>和List<Integer>在運行事實上是相同的類型。 他們都被擦除成他們的原生類型,即List。 因爲編譯的時候會有類型擦除, 所以不能通過同一個泛型類的實例來區分方法, 如下面的例子編譯時會出錯, 因爲類型擦除後, 兩個方法都是List 類型的參數, 因此並不能
根據泛型類的類型來區分方法。

/*會導致編譯時錯誤*/
 public class Erasure{
 public void test(List<String> ls){
 System.out.println("Sting");
 }
 public void test(List<Integer> li){
 System.out.println("Integer");
 }
 }
(4) 所有泛型類的實例都共享同一個運行時類, 類型參數信息會在編譯時被擦除。 因此考
慮如下代碼, 雖然 ArrayList<String>和 ArrayList<Integer>類型參數不同, 但是他們都共享
ArrayList 類, 所以結果會是 true。
List<String>l1 = new ArrayList<String>();
List<Integer>l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass()); //True
(5) 不能對確切的泛型類型使用instanceOf 操作。因爲instanceof會在運行時檢測對象的類型,而泛型在運行時已經被擦除了,所以所有的List都是一樣的,set也都是一樣的。如下面的操作是非法的, 編譯時會出錯。

Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>){…}// compile error.如果改成instanceof Collection<?>則不會出錯。
(6) 不能用基本類型實例化類型參數, 因此嗎, 沒有 Pair<double>,只有Pair<Double>,
因是類型擦除, 擦除後, Pair類含有Object類型的域, 而Object不能存儲doble 值。

2、泛型和子類型

List<Apple> apples = new ArrayList<Apple>(); //right
List<Fruit> fruits = apples; //error
我們假定第2行代碼沒有問題, 那麼我們可以使用語句fruits.add(new Strawberry())
Strawberry Fruit的子類) 在fruits中加入草莓了, 但是這樣的話, 一個List中裝入了
各種不同類型的子類水果, 這顯然是不可以的, 因爲我們在取出
List中的水果對象時, 就
分不清楚到底該轉型爲蘋果還是草莓了。
通常來說,如果
FooBar的子類型,G是一種帶泛型的類型,則G<Foo>不是G<Bar>
的子類型。

3、通配符

(1)<?>非限定通配符

//使用通配符? , 表示可以接收任何元素類型的集合作爲參數
 public void printCollection(Collection<?> c) {
 for (Object e:c) {
 System.out.println(e);
 }
 }
這裏使用了通配符? 指定可以使用任何類型的集合作爲參數。 讀取的元素使用了 Object 類型來表示, 這是安全的, 因爲所有的類都是 Object 的子類。 這裏就又出現了另外一個問題,如下代碼所示, 如果試圖往使用通配符? 的集合中加入對象, 就會在編譯時出現錯誤。 需要注意的是, 這裏不管加入什麼類型的對象都會出錯。這是因爲通配符? 表示該集合存儲的元素類型未知, 可以是任何類型。 往集合中加入元素需要是一個未知元素類型的子類型, 正因爲該集合存儲的元素類型未知, 所以我們沒法向該集合中添加任何元素。唯一的例外是null,因爲null是所有類型的子類型, 所以儘管元素類型不知道, 但是null一定是它的子類型。
Collection<?> c=new ArrayList<String>();
c.add(newObject()); //compile time error, 不管加入什麼對象都出錯, 除了 null 外。
c.add(null); //OK
(2)限制性通配符:
<? extends A>從一個數據類型裏獲取數據
<? super B>把對象寫入一個數據結構裏
假定有一個畫圖的應用, 可以畫各種形狀的圖形, 如矩形和圓形等。 爲了在程序裏面表示,
定義如下的類層次:

public abstract class Shape {
 public abstract void draw(Canvas c);
 }
 public class Circle extends Shape {
 private int x,y,radius;
 public void draw(Canvas c) { ... }
 }
 public class Rectangle extends Shape
 private int x,y,width,height;
 public void draw(Canvas c) { ... }
 }
如果我們希望在List<?exends Shape> shapes 中加入一個矩形對象, 如下所示:

shapes.add(0, new Rectangle()); //compile-time error

那麼這時會出現一個編譯時錯誤, 原因在於: 我們只知道 shapes 中的元素時 Shape類型的子類型, 具體是什麼子類型我們並不清楚, 所以我們不能往shapes中加入任何類型的對象。不過我們在取出其中對象時, 可以使用 Shape 類型來取值, 因爲雖然我們不知道列表中的元素類型具體是什麼類型, 但是我們肯定的是它一定是Shape類的子類型。 所以可以用Shape接收取出的對象, 但不能向其中添加對象。

List<Shape> shapes = new ArrayList<Shape>();
List<? super Cicle> cicleSupers = shapes;
cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OK
cicleSupers.add(new Shape()); //ERROR
這表示cicleSupers 列表存儲的元素爲 Circle 的超類, 因此我們可以往其中加入 Circle對象或者Circle 的子類對象, 但是不能加入 Shape 對象。 這裏的原因在於列表 cicleSupers存儲的元素類型爲Cicle 的超類, 但是具體是 Cicle 的什麼超類並不清楚。 但是我們可以確定的是只要是 Cicle 或者 Circle 的子類, 則一定是與該元素類別兼容。

4、泛型方法

泛型方法的格式, 類型參數<T>需要放在函數返回值之前。 然後在參數和返回值中就可以使用泛型參數了。

public static <T> void fromArrayToCollection(T[] a, Collection<T>c){
 for(T o : a) {
 c.add(o);// correct
 }
 }
調用方法如下:
Object[] oa = new Object[100];
Collection<Object>co = new ArrayList<Object>();
fromArrayToCollection(oa, co);// T inferred to be Object
我們調用方法時並不需要傳遞類型參數,系統會自動判斷類型參數並調用合適的方法。 當然
在某些情況下需要指定傳遞類型參數, 比如當存在與泛型方法相同的方法的時候( 方法參數
類型不一致) , 如下面的一個例子:
public <T> void go(T t) {
 System.out.println("generic function");
 }
 public void go(String str) {
 System.out.println("normal function");
 }
 public static void main(String[] args) {
 FuncGenric fg = new FuncGenric();
 fg.go("haha");//打印 normal function
 fg.<String>go("haha");//打印 generic function
 fg.go(new Object());//打印 generic function
 fg.<Object>go(new Object());//打印 generic function
 }

5、方法重載

在 JAVA 裏面方法重載是不能通過返回值類型來區分的, 比如代碼一中一個類中定義兩個如
下的方法是不容許的。 但是當參數爲泛型類型時, 卻是可以的。 如下面代碼所示, 雖然形參
經過類型擦除後都爲 List 類型, 但是返回類型不同, 這是可以的。

/*代碼: 正確 */
 public class Erasure{
 public void test(List<String> ls){
 System.out.println("Sting");
 }
 public int test(List<Integer> li){
 System.out.println("Integer");
 }
 }

6、泛型數組

不能創建一個確切泛型類型的數組。 如下面代碼會出錯。
List<String>[] lsa = new ArrayList<String>[10]; //compile error.
因爲如果可以這樣, 那麼考慮如下代碼, 會導致運行時錯誤。

 List<String>[] lsa = new ArrayList<String>[10]; // 實際上並不允許這樣創建數組
 Object o = lsa;
 Object[] oa = (Object[]) o;
 List<Integer>li = new ArrayList<Integer>();
 li.add(new Integer(3));
 oa[1] = li;// unsound, but passes run time store check
 String s = lsa[1].get(0); //run-time error - ClassCastException
因此只能創建帶通配符的泛型數組, 如下面例子所示, 這回可以通過編譯, 但是在倒數第二行代碼中必須顯式的轉型才行, 即便如此, 最後還是會拋出類型轉換異常, 因爲存儲在 lsa中的是 List<Integer>類型的對象, 而不是 List<String>類型。 最後一行代碼是正確的, 類型匹配, 不會拋出異常。

 List<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type
 Object o = lsa;
 Object[] oa = (Object[]) o;
 List<Integer>li = new ArrayList<Integer>();
 li.add(new Integer(3));
 oa[1] = li; //correct
 String s = (String) lsa[1].get(0);// run time error, but cast is explicit
 Integer it = (Integer)lsa[1].get(0); // OK

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

既不能拋出也不能捕獲泛型類對象, 甚至泛型類擴展 Throwable 都是不合法的。如, 以下定義不能正常編譯:
public class Problem<T> extends Exception{}//Error
catch 子句中不能使用類型變量, 以下方法不能編譯:
public static <T extends Throwable> void dowork(class<T> t)
{
 try {}
 catch(T e) //error
 {}
}
過, 在異常規範中使用類型變量是允許的, 以下方法合法:
Public static <T extends Throwable> void doWork(T t) throws T
 try{}
 Catch(Throwable realCause)
 {
   t.initCause(realCause);
   throw t;
  } 
}

二、使用規範

第23條:不要在新代碼中使用原生態類型

  • 在1.5版本以後的代碼建議使用泛型而不是對應的原生態類型,這有助於錯誤的發現,使用Java的特性,避免強制轉換。是用原生態類型失去了泛型在安全性和表述性方面的所有優勢。

  • 如果一些情況下客戶代碼不在乎類型參數T到底是什麼的話,也建議使用泛型,如<?>,表示接受任何類型,相比於“T”限制了某一類型,“?”的範圍過得多(?被成爲無限制的通配符)。

總之,使用原生態類型會在運行時導致異常,因此不要在新代碼中使用。Set<Object>是個參數化類型,表示可以包含任何對象類型的一個集合;Set<?>則是一個通配符類型,表示只能包含某種未知對象類型的一個集合,Set則是原生態類型,他脫離了泛型系統。前兩種是安全的,最後一中不安全。

第24條:消除非受檢警告

在使用泛型時,會遇到許多編譯器警告:非受檢強制轉化警告、非受檢方法調用警告、非受檢普通數組創建警告、以及非受檢轉換警告。 
如果無法消除警告,同時可以證明引起警告的代碼是類型安全的,(只有在這種情況下)可以用一個@SuppressWarnings("unchecked")註解來禁止這條警告。
SuppressWarnings註解可以用在任何粒度的級別中,從單獨的局部變量聲明到整個類都可以。應該始終在儘可能小的範圍中使用SuppressWarnings註解。它通常是個變量聲明,或是非常簡短的方法或者構造器。永遠不要再整個類上使用SuppressWarnings,那麼做可能會掩蓋了重要的警告。

第25條:列表優先於數組

數組和泛型的區別:
(1)數組是協變的,即如果Sub爲Super的子類型,那麼數組類型Sub[]就是Super[]的子類型。相反,泛型則是不可變的,即對於任意兩個不同的類型Type1和Type2,List<Type1>既不是List<Type2>的子類型,也不是List<Type2>的超類型。
(2)數組是具體化的,因此數組會在運行時才知道並檢查他們的元素類型約束。而泛型則是通過擦除實現的,因此泛型只在編譯時強化它們的類型信息,並在運行時丟棄(或擦除)它們的元素類型信息。
由於數組和泛型之間的根本區別,數組和泛型不能很好的混合使用。創建泛型、參數化類型或者類型參數的數組都是非法的。如果混合使用出現了編譯時錯誤或者警告,那麼應該用列表代替數組

第26條:優先考慮泛型

因爲聲明一個泛型數組是合法的,而new一個泛型數組是不合法的,那麼當自定義一個泛型時,內部需要使用到泛型數組,如何new呢?
一種方法是先new 一個Object數組,然後強制轉換成對應的泛型的,編譯器會警告這不是類型安全的,但我們在檢查後確認無誤,可以這樣使用。
例如:E[] elements = (E[])new Object[100];
第二種方法是:將E[]改爲Object[],即Object[] elements = new Object[100];但是在獲取對象後,要將對象強轉爲E即E result = (E)elements[0];這樣做編譯器會警告這是不安全的,可以通過@SuppressWarnings("unchecked")來解除警告。

第27條:優先考慮泛型方法

使用泛型方法可以不用在代碼中顯示進行類型轉換,還可以進行類型推導。

(1).編寫一個集合並集的泛型方法:

public static <E> Set<E> union(Set<E> s1, Set<E> s2){  
    Set<E> result = new HashSet<E>(s1);  
    result.addAll(s2);   
    return result;  
}  

(2).泛型類型推導:

正常聲明泛型集合如下:

Map<String, List<String>>anagrams = new HashMap<String, List<String>>();

通過集合聲明時的泛型參數類型就可以推導出集合實例的泛型參數類型,這個過程叫泛型類型推導,如果支持泛型類型推導,則上面代碼的HashMap就可以不再指定泛型參數類型,但是目前JDK還沒有內置泛型類型推導,我們可以自己進行一個小的模擬實現:

public static <K, V> HashMap<K, V> newHashMap(){  
    return new HashMap<K, V>();  
}  
Map<String, List<String>> anagrams = newHashMap();  

泛型單例工廠

泛型單例工廠模式用於創建不可變但又適合於許多不同類型的對象,由於泛型是通過類型檫除實現的,因此可以給所有必要的類型參數使用單個對象,例子如下:

public interface UnaryFunction<T>{  
    T apply(T arg);  
}  
private static UnaryFunction<Object> IDENTITY_FUNCTION =   
new UnaryFunction<Object>(){  
    public Object apply(Object arg){  
    return arg;  
}  
}  
public static <T> UnaryFunction<T> identityFunction(){  
    return (UnaryFunction<T>) IDENTITY_FUNCTION;  
}  

由於泛型是類型檫除的,在運行時對於無狀態的泛型參數類型只需要一個泛型單例即可。

泛型遞歸類型限制

使用泛型可以通過某個包含該類型參數本事的表達式來限制類型參數,如

<T extends Comparable<T>>

讀作“針對可以與自身進行比較的每個類型T”,即互比性。

下面的例子是找出列表中實現了Comparable接口的元素的最大值:

public static <T extends Comparable<T>> T max(List<T> list){  
    Iterator<T> i = list.iterator();  
    T result = i.next();  
    while(i.hasNext()){  
    T t = i.next();  
    if(t.compareTo(result) > 0){  
    result = t;  
}  
return result;  
}  
}  

第28條:利用有限制通配符來提升API的靈活性

由於泛型參數化類型是不可變的,對於任何類型的Type1和Type2而言,List<Type1>既不是List<Type2>的子類型,也不是它的超類型,由此會產生可以將任何對象放進List<Object>中,卻只能將字符串放在List<String>中的問題,解決此類問題我們需要使用泛型的通配符。

例子如下:

自定義堆棧的API如下:

public class Stack<E> {  
    public Stack();  
    public void push(E e);  
    public E pop();  
    public boolean isEmpty();  
    public void pushAll(Iterable<E>  src){  
    for(E e : src){  
    push(e);  
}  
}  
    public void popAll(Collection<E> dst){  
    while(!isEmpty()){  
    dst.add(pop());  
}  
}  
}  

上述代碼編譯完全沒有問題,但是如果想完美運行還需要使用泛型通配符。

(1).生產者限制通配符extends:

使用如下的測試數據對pushAll方法進行測試:

Stack<Number> numberStack = new Stack<Number>();  
Iterable<Integer> integers = …;  
numberStack.pushAll(integers);  
在運行時pushAll方法會報參數類型不匹配錯誤,解決這個問題可以使用限制通配符類型,將pushAll方法修改如下:
public void pushAll(Iterable<? extends E>  src){  
    for(E e : src){  
    push(e);  
}  
}  

Iterable<? extends E>的意思是集合元素的類型是自身的子類型,即任何E的子類型,在本例子Integer是Number的子類,因此正好符合此意。

(2).消費者限制通配符super:

使用下面的測試數據對popAll方法進行測試:

Stack<Integer> integerStack = new Stack< Integer>();  
Iterable<Number> numbers = …;  
integerStack.popAll(numbers);  
在運行時popAll方法會報參數類型不匹配錯誤,解決這個問題可以使用限制通配符類型,將popAll方法修改如下:
public void popAll(Collection<? super E> dst){  
    while(!isEmpty()){  
    dst.add(pop());  
}  
}  

Collection<? super E>的意思是集合元素的參數類型是自身的超類型,即任何E的超類,在本例中可以將Integer類型的元素添加到其超類Number的集合中。

上述的兩個通配符可以簡記爲PECS原則,即producer-extends,consumer-super.

(3).無限制通配符?:

對於同時具有生產者和消費者雙重身份的對象來說,無限制通配符?更合適,一個交互集合元素的方法聲明如下:

public static void swap(List<?> list, int i, list j);  

一般來說,如果類型參數只在方法聲明中出現一次,就可以使用通配符取代它,如果是無限制的類型參數,就使用無限制通配符?代替。

類型安全的異構容器

第29條:優先考慮類型安全的異構容器

一般情況下,集合容器的只能由固定的類型參數,如一個Set只有一個類型參數表示它的類型,一個Map有兩個類型參數表達鍵和值的類型,但是有些情況下我們需要更多的靈活性,即將容器的鍵進行參數化而不是將容器參數化,然後將參數化的鍵提交給容器,來插入或者獲取值,用泛型系統來確保值的類型與它的鍵類型相符。

在JDK1.5之後Class被泛化了,類的類型從字面上來看不再只是簡單的Class,而是Class<T>,例如String.class屬於Class<String>類型,Integer.class屬於Class<Integer>類型。

public class Favorites{
private Map<Class<?>, Object> favorites = new HashMap<Class<?>, Object>();
public <T> void putFavorites(Class<T> type, T instance){
if(type == null){
throws new NullPointerException(“Type is null”);
}
favorites.put(type, type.cast(instance));
}
public <T> T getFavorite(Class<T> type){
return type.cast(favorites.get(type));
}
public static void main(String[] args){
Favorites f = new Favorites();
putFavorite(String.class, “java”);
putFavorite(Integer.class, 0xcafebabe);
putFavorite(Class.class, Favorite.class);
String favoriteString = f.getFavorite(String.class);
Int favoriteInteger = f.getFavorite(Integer.class);
Class<?> favoritesClass = f.getFavorite(Class.class);
System.out.printf(“%s %x %s%n”, favoriteString, favoriteInteger, favoritesClass);
}
}

程序正常打印出 Java cafebabe Favorites。


發佈了108 篇原創文章 · 獲贊 20 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章