我的博客即將入駐“雲棲社區”,誠邀技術同仁一同入駐。
參考書籍
《Java核心技術:卷1》
泛型, 先睹為快
先通過一個簡單的例子說明下Java中泛型的用法:
泛型的基本形式類似於模板, 通過一個類型參數T, 你可以"私人定制"一個類,具體定制的范圍包括實例變量的類型,返回值的類型和傳入參數的類型
Foo.java
public class Foo <T> { // 約定實例變量的類型 private T data; // 約定返回值的類型 public T getData () { return this.data; } // 約定傳入參數的類型 public void setData (T data) { this.data = data; } }
Test.java
public class Test { public static void main (String args[]) { Foo<String> s = new Foo<String> (); } }
泛型的由來
泛型設計源於我們的編寫類時的一個剛需:想讓我們編寫的處理類能夠更加"通用", 而不是只能處理某些特定的對象或場景。或者說:我們希望我們的類能實現盡可能多的復用。舉個栗子:一般來說,你並不想要編寫多個分別處理不同數據類型,但內在邏輯代碼卻完全一樣的類。因為這些處理類可能除了數據類型變換了一下外,所有代碼都完全一致。“只要寫一個模板類就OK了嘛~ 等要使用的時候再傳入具體的類型,多省心”, 當你這么思考的時候:浮現在你腦海里的,就是泛型程序設計(Generic pogramming)的思想
在介紹Java的泛型機制之前, 先讓我們來看看, 還沒加入泛型機制的“泛型程序設計”是怎樣子的
泛型程序設計1.0: 不用Java泛型機制
下面我們編寫一個存儲不同的對象的列表類,列表有設置(set)和取值(get)兩種操作。
假設這個列表類為ObjArray,同時嘗試存儲的值為String類型,則:
1.在ObjArray類里我們維護一個數組arr, 為了將來能容納不同的對象, 將對象設為Object類型(所有對象的父類)
2.在實例化ObjArray后, 通過調用set方法將String存入Object類型的數組中; 而在調用get方法時, 要對取得的值做強制類型轉換—從Object類型轉為String類型
ObjArray.java:
public class ObjArray { private Object [] arr; public ObjArray(int n) { this.arr = new Object[n]; } public void set (int i, Object o) { this.arr[i] = o; } public Object get (int i) { return this.arr[i]; } }
Test.java:
/** * @description: 測試代碼 */ public class Test { public static void main (String args[]) { ObjArray arr = new ObjArray(3); arr.set(0, "彭湖灣"); // get操作時要做強制類型轉換 String n =(String)arr.get(0); // 輸出 "彭湖灣" System.out.print(n); } }
如果不使用泛型機制,但又想要實現泛型程序設計,就會編寫出類似這樣的代碼。
泛型程序設計2.0: 使用Java泛型機制
讓我們來看看使用泛型機制改進后的結果。
看起來大約是這樣:
GenericArray.java
public class GenericArray<T> { public void set (int i, T o) { // ... } public T get (int i) { // ... } }
【具體代碼下面給出】
Test.java:
public class Test { public static void main (String args[]) { GenericArray<String> arr = new <String>GenericArray(3); arr.set(0, "彭湖灣"); // 不用做強制類型轉換啦~~ String s =arr.get(0); // 輸出: 彭湖灣 System.out.print(s); } }
我們發現,改進后的設計有以下幾點好處:
1. 規范、簡化了編碼: 我們不用在每次get操作時候都要做強制類型轉換了
2. 良好的可讀性:GenericArray<String> arr這一聲明能清晰地看出GenericArray中存儲的數據類型
3. 安全性:使用了泛型機制后,編譯器能在set操作中檢測傳入的參數是否為T類型, 同時檢測get操作中返回值是否為T類型,如果不通過則編譯報錯
泛型並非無所不能
了解到了泛型的這些特性后, 也許你會迫不及待地想要在ObjArray類里大干一場。
例如像下面這樣, 用類型參數T去直接實例化一個對象, 或者是實例化一個泛型數組
可惜的是 ......
public class GenericArray<T> { private T obj = new T (); // 編譯報錯 private T [] arr = new T[3]; // 編譯報錯 // ... }
沒錯, 泛型並不是無所不能的, 相反, 它的作用機制受到種種條框的限制。
這里先列舉泛型機制的兩個限制:
1.不能實例化類型變量, 如T obj = new T ();
2. 不能實例化泛型數組,如T [] arr = new T[3];
【注意】這里不合法僅僅指實例化操作(new), 聲明是允許的, 例如T [] arr
我們現在來繼續看看上面泛型設計中, GenericArray類的那部分代碼:
public class GenericArray<T> { private Object [] arr; public GenericArray(int n) { this.arr = new Object[n]; } public void set (int i, T o) { this.arr[i] = o; } public T get (int i) { return (T)this.arr[i]; } }
沒錯, 在ObjArray類內部我們仍然還是用到了強制轉型。看到這里也許令人有那么一點點的小失望, 畢竟還是沒有完全跳出
初始的泛型設計的邊界。 但是, 泛型的優點仍然是顯而易見的, 只不過要知道的是:它並沒有無所不能的魔法, 並受到諸多限制。
泛型的編寫規則
1.泛型類和泛型方法的定義
泛型類
如前面所說,可以像下面一樣定義一個泛型類
類型變量T放在類名的后面
public class Foo <T> { // 約定實例變量的類型 private T data; // 約定返回值的類型 public T getData () { return this.data; } // 約定傳入參數的類型 public void setData (T data) { this.data = data; } }
泛型方法
也可以定義一個泛型方法:
泛型變量T放在修飾符(這里是public static)的后面, 返回類型的前面
public class Foo { public static <T> T getSelf (T a) { return a; } }
泛型方法可以定義在泛型類當中,也可以定義在一個普通類當中
2.可以使用多個類型變量
public class Foo<T, U> { private T a; private U b; }
【注意】在Java庫中,常使用E表示集合的元素類型, K和V分別表示關鍵字和值的類型, T(U,S)表示任意類型
3.JavaSE7以后,在實例化一個泛型類對象時,構造函數中可以省略泛型類型
ObjArray<Node> arr = new <Node>ObjArray();
可簡寫成:
ObjArray<Node> arr = new <>ObjArray();
類型變量的限定
當我們實例化泛型類的時候, 我們一般會傳入一個可靠的類型值給類型變量T。 但有的時候,被定義的泛型類作為接收方,也需要對傳入的類型變量T的值做一些限定和約束,例如要求它必須是某個超類的子類,或者必須實現了某個接口, 這個時候我們就要使用extends關鍵字了。如:
超類SuperClass:
public class SuperClass {
}
子類SubClass:
public class SubClass extends SuperClass { }
對T使用超類類型限定:要求父類必須為SuperClass
public class Foo<T extends SuperClass> { }
測試:
public class Test { public static void main (String args[]) { Foo<SubClass> f = new Foo<SubClass>(); // 通過 Foo<String> n = new Foo<String>(); // 報錯 } }
extends使用的具體規則
1. 對於要求實現接口, 或者繼承自某個父類, 統一使用extends關鍵字 (沒有使用implements關鍵字,為了追求簡單)
2. 限定類型之間用 "&" 分隔
3. 如果限定類型既有超類也有接口,則:超類限定名必須放在前面,且至多只能有一個(接口可以有多個)
這個書寫規范和類的繼承和接口的實現所遵循的規則是一致的(<1>不允許類多繼承,但允許接口多繼承<2>書寫類的時候類的繼承是寫在接口實現前面的)
// 傳入的T必須是SuperClass的子類,且實現了Comparable接口 public class Foo<T extends SuperClass&Comparable> { }
【注意】: 上面的SuperClass和Comparable不能顛倒順序
泛型類的繼承關系
泛型類型的引入引發了一些關於泛型對象繼承關系的有趣(?)問題。
在Java中, 如果兩個類是父類和子類的關系,那么子類的實例也都是父類的實例,這意味着:
一個子類的實例可以賦給一個超類的變量:
SubClass sub = new SubClass(); SuperClass sup = sub;
當引入了泛型以后, 有趣(?)的問題來了:
我們通過兩對父子類List/ArrayList, Employee/Manager來說明這個問題
(我們已經知道List是ArrayList的父類(抽象類),這里假設Employee是Manager的父類)
1. ArrayList<Employee> 和 ArrayList<Manager>之間有繼承關系嗎?(ArrayList<Manager>的實例能否賦給ArrayList<Employee>變量?)
2. List<Employee> 和 ArrayList<Employee>之間有繼承關系嗎?(ArrayList<Employee>的實例能否賦給 List<Employee>變量?)
3. ArrayList和ArrayList<Employee>之間有繼承關系嗎?(ArrayList<Employee>的實例能否賦給ArrayList變量?)
答案如下:
對1: 沒有繼承關系; 不能
ArrayList<Manager> ae = new ArrayList<Manager>(); ArrayList<Employee> am = ae; // 報錯
對2: 有繼承關系; 可以
ArrayList<Employee> al = new ArrayList<>(); List<Employee> l = al; // 通過
對3: 有繼承關系; 可以
ArrayList<Employee> ae = new ArrayList<>(); ArrayList a = ae;
下面用三幅圖描述上述關系:
描述下1,2的關系
對上面三點做個總結:
1. 類名相同,但類型變量T不同的兩個泛型類沒有什么聯系,當然也沒有繼承關系(ArrayList<Manager>和ArrayList<Employee>)
2. 類型變量T相同,同時本來就是父子關系的兩個類, 作為泛型類依然保持繼承關系 (ArrayList<Employee>和List<Employee>)
3. 某個類的原始類型,和其對應的泛型類可以看作有“繼承關系”(ArrayList和ArrayList<Employee>)
引用一幅不太清晰的圖
通配符?
泛型繼承關系帶來的問題 — 類型匹配的苦惱
問題出現在上面所述規范中的第二點:ArrayList<Manager> 和 ArrayList<Employee>之間沒有繼承關系。
這意味着,如果你像下面一樣編寫一個處理ArrayList<Employee>的方法
public class Foo { public static void handleArr (ArrayList<Employee> ae) { // ... } }
你將無法用它來處理ArrayList<Manager>:
public static void main (String args[]) { ArrayList<Manager> am = new ArrayList<Manager>(); Foo.handleArr(am); // 報錯,類型不匹配 }
現在我們想要:“handleArr方法不僅僅能處理ArrayList<Employee>, 而且還能處理ArrayList<X> (這里X代表Employee和它子類的集合)”。於是這時候通配符?就出現了:ArrayList<? extends Employee>能夠匹配ArrayList<Manager>, 因為ArrayList<? extends Employee>是 ArrayList的父類
現在我們的例子變成了:
public class Foo { public static void handleArr (ArrayList<? extends Employee> ae) { // ... } } public static void main (String args[]) { ArrayList<Manager> am = new ArrayList<Manager>(); Foo.handleArr(am); // 可以運行啦! }
通配符和super關鍵字
?統配不僅可以用於匹配子類型, 還能用於匹配父類型:
<? super Manager>
泛型的其他約束
上面我們介紹了泛型的一些約束,例如不能直接實例化實例化類型變量和泛型數組,這里和其他約束一起做個總結:
在定義泛型類時不能做的事:
1. 不能實例化類型變量
T obj = new T (); // 報錯, 提示: Type parameter 'T' cannot be instantiated directly
解決方案:
如果實在要創建一個泛型對象的話, 可以使用反射:
public class GenericObj<T> { private T obj; public GenericObj(Class<T> c){ try { obj = c.newInstance(); // 利用反射創建實例 } catch (Exception e) { e.printStackTrace(); } } }
/** * @description: 測試代碼 */ public class Test { public static void main (String args[]) { // 通過 GenericObj<String> go = new GenericObj<> (String.class); } }
因為Class類本身就是泛型, 而String.class是Class<T>的實例,
2. 不能實例化泛型數組,如T [] arr = new T[3];
private T [] arr = new T[3]; // 報錯, 提示: Type parameter 'T' cannot be instantiated directly
解決方法一:
上文所提到的,創建Object類型的數組,然后獲取時轉型為T類型:
public class GenericArray<T> { private Object [] arr; public GenericArray(int n) { this.arr = new Object[n]; } public void set (int i, T o) { this.arr[i] = o; } public T get (int i) { return (T)this.arr[i]; } }
解決方法二: 利用反射
這里使用反射機制中的Array.newInstance方法創建泛型數組
GenericArray.java
public class GenericArray<T> { private T [] arr; public GenericArray(Class<T> type, int n) { arr = (T[])Array.newInstance(type, n); // 利用反射創建泛型類型的數組 } public void set (int i, T o) { this.arr[i] = o; } public T get (int i) { return (T)this.arr[i]; } }
Test.java
/** * @description: 測試代碼 */ public class Test { public static void main (String args[]) { GenericArray<String> genericArr = new GenericArray<>(String.class, 5); genericArr.set(0, "penghuwan"); System.out.println(genericArr.get(0)); // 輸出 "penghuwan" } }
3. 不能在泛型類的靜態上下文中使用類型變量
public class Foo<T> { private static T t; public static T get () { // 報錯, 提示: 'Foo.this' can not be referenced from a static context return T; } }
注意這里說的是泛型類的情況下。如果是在一個靜態泛型方法中是可以使用類型變量的
public class Foo { public static<T> T get (T t) { // 通過 return t; } }
(這里的泛型方法處在一個非泛型類中)
4. 不能拋出或者捕獲泛型類的實例
不能拋出或者捕獲泛型類的實例:
// 報錯 提示:Generic class may not extend java.lang.throwable public class Problem<T> extends Exception { }
甚至擴展Throwable也是不合法的
public class Foo { public static <T extends Throwable> void doWork () { try { // 報錯 提示: Cannot catch type parameters }catch (T t) { } } }
但在異常規范中使用泛型變量是允許的
// 能通過 public class Foo { public static <T extends Throwable> void doWork (T t) throws T { try { // ... }catch (Throwable realCause) { throw t; } } }
在使用泛型類時不能做的事
1. 不能使用基本類型的值作為類型變量的值
Foo<int> node = new Foo<int> (); // 非法
應該選用這些基本類型對應的包裝類型
Foo<Integer> node = new Foo<Integer> ();
2. 不能創建泛型類的數組
public static void main (String args[]) { Foo<Node> [] f =new Foo<Node> [6]; // 報錯 }
解決方法:
可以聲明通配類型的數組, 然后做強制轉換
Foo<Node> [] f =(Foo<Node> [])new Foo<?> [6]; // 通過