Java中的紙老虎之泛型
泛型,其實算是Java當中比較難的語法瞭,很多人一開始都對其一知半解,也很害怕閱讀帶泛型的源碼,雖然看起來語法很難,但當你理解後會覺得很簡單,其實隻是一個紙老虎罷瞭。下面,我將會用非常簡單易懂的方式帶你去理解它,相信你在認真看完後會有非常大的收獲,從此不會再畏懼它!
一. 泛型的定義
這裡大傢可以不必去看網上的有些定義,因為相對於比較學術化,隻需記住泛型可以在程序設計中指定某種類型,讓程序的設計更加規范化即可
二. 為什麼要用到泛型
瞭解到瞭泛型是什麼後,那我們來討論討論為什麼要用泛型這個語法,這個語法到底是幹什麼的?別急,下面,我先給大傢舉一個例子:
class Stack { public Object[] objects; public int top; public Stack() { this.objects =new Object[10]; } public void push(Object obj) { objects[this.top++] = obj; } public Object get() { return objects[this.top-1]; } }
大傢可以看看這是在幹什麼呢?這是我們自己寫瞭一個棧,然後將棧裡的數組類型設置成Object類型,這樣的話這個棧裡任意類型的數據都可以存放瞭(Object類是任何類的父類,不管插入什麼類型的數據,都可以發生向上轉型)
下面,我們來測試一下
public class Test { public static void main(String[] args) { Stack stack=new Stack(); stack.push(1); stack.push(2); stack.push("123"); String str=(String)stack.get(); } }
可以看到,我們可以向自己寫的棧裡放入整形以及字符串等等任何類型的數據,但註意一下取出數據的時候要進行強制類型轉換
以上這樣寫,可以向棧裡存放任何類型的數據,比較通用,其優點也可以變成缺點,正因為太通用瞭,使代碼的規范性降低,看起來比較凌亂,這時候,我們可以考慮使用泛型,這樣可以在類中或者Java集合中存放特定的數據(使用Java集合時,一般都要用到泛型,而自定義的類型中可以使用泛型也可以不使用)
三. 泛型的寫法
以自定義的類型為例,寫法為在類名後面加上尖括號,裡面寫上一個字母(註意,此處寫任何字母都可以,隻起到一個標記這個類為泛型類的作用)
class Stack<T>
而在new對象時,以棧裡隻能存放整形為例,前面的尖括號必須寫基本數據類型對應的包裝類,而後面的尖括號可以不用寫,示例如下:
Stack<Integer> stack = new Stack<>();
補一下Java中的基本數據類型與對應的包裝類:
因此,我們前面寫的自定義的棧可以寫成以下形式(以存放整形為例):
class Stack<T> { public T[] objects; public int top; public Stack() { this.objects = (T[])new Object[10]; } public void push(T obj) { objects[this.top++] = obj; } public T get() { return objects[this.top-1]; } } Stack<Integer> stack = new Stack<>(); stack.push(1); stack.push(2); int ret = stack.get(); System.out.println(ret);
特別註意此處:public Stack() { this.objects = (T[])new Object[10]; }
這裡不能寫成this.objects=new T[10];
原因:
1. 不能new泛型類型的數組
2. 也可理解為泛型是先檢查後編譯的,如果new泛型類型的數組的話,編譯器檢查時並不知道T是什麼類型的,因此會報錯。而編譯的時候才會進行擦除機制,都會將其轉換為Object類型
3. 正因為有這個擦除機制,這裡才能進行數組整體強制類型轉換(一般數組不能整體進行強制類型轉換),因為泛型隻是在編譯的時候起作用,而實際運行時都會被擦除成Object類型,即實際運行時是沒有泛型這個概念的,也即實際運行時類型都是一樣的,所以T本質上是object類型的,所以此代碼等價於不進行強制類型轉換!!!
4.而直接指定泛型的代碼(不是T) 比如:Stack<Integer>和Stack<Character>都是在運行時直接把尖括號裡的類型擦掉瞭,可以看到直接打印的結果(並沒有打印出類型):
此處註意多理解理解
四. 泛型的使用實例
1. 求最大值
以上就是泛型的一個重要知識點瞭,但光看是不夠的,還是得通過例子讓大傢有一個更為深入的理解,比如,如何寫一個泛型類來求數組的最大值呢?
基本的框架大概是這樣的:(沒看懂的小可愛好好看看上面講的內容哦)
class Algorithm<T extends Comparable<T>> { public T findMax(T[] array) { T max = array[0]; for (int i = 1; i < array.length; i++) { if(max < array[i]) { max = array[i]; } } return max; } }
但是此代碼if(max < array[i])
會報錯,為什麼呢?因為將來給T傳的值一定是一個引用類型,引用類型不能直接比較大於或者小於的,是要用Comparable或Comparator接口裡的方法比較的,
因為泛型在編譯的時候會被擦除成Object類型,但Object類本身並沒有實現Comparable
或Comparator
接口,所以我們要控制其不要擦除到Object類,所以要給泛型指定一個邊界
具體寫法如下:
class Algorithm<T extends Comparable<T>> { public T findMax(T[] array) { T max = array[0]; for (int i = 1; i < array.length; i++) { //max < array[i] if(max.compareTo(array[i]) < 0) { max = array[i]; } } return max; } }
class Algorithm<T extends Comparable<T>>
註意,extends叫做上界,此代碼代表的意思為T這個泛型類會擦除到實現瞭Comparable接口的地方,換句話說,這個T類型一定是實現瞭Comparable接口的
同理:這個代碼public class MyArrayList<E extends Number> { ... }
代表E為Number的子類或Number本身
下面讓我們來用一下:
Algorithm<Integer> algorithm1 = new Algorithm<>(); Integer[] integers = {1,2,13,4,5}; Integer ret = algorithm1.findMax(integers); System.out.println(ret);
運行結果如下:
成功瞭!
2. 優化
經過上面的努力,我們已經寫出瞭一個泛型類來求一個數組的最大值瞭,但是,上面的例子是一個整形數組,那麼我們能不能在數組裡存放別的類型去比較呢?答案是可以的,但是我們還得去new一個對象,例如:Algorithm<String> algorithm2 = new Algorithm<>();
這樣很麻煩。但是我們可以將求最大值的方法設置成靜態的class Algorithm2 <T>
,因為是靜態的方法,不需要new對象,所以就沒有在new對象時指定泛型的過程瞭,所以沒必要給方法後加尖括號,但是去掉之後,代碼又會被錯:
我們可以這樣修改:
class Algorithm2 { public static<T extends Comparable<T>> T findMax(T[] array) { T max = array[0]; for (int i = 1; i < array.length; i++) { if(max.compareTo(array[i]) < 0) { max = array[i]; } } return max; } }
此方法public static<T extends Comparable<T>> T findMax(T[] array){}
叫做泛型方法
下面繼續帶大傢來用一下:
public static void main(String[] args) { Integer[] integers = {1,2,13,4,5}; //會根據形參的類型推導出整個泛型的類型參數 Integer ret = Algorithm2.findMax(integers); System.out.println(ret); Integer ret2 = Algorithm2.<Integer>findMax(integers); System.out.println(ret2); }
註意,ret1寫法和ret2寫法是一樣的,都可以
打印結果如下:
五. 通配符
1. 基本寫法
通配符也是泛型的一種,下面我們來寫一個泛型方法來打印集合中的元素
class Test { public static<T> void print(ArrayList<T> list) { for (T t : list) { System.out.println(t); } }
這個寫法很簡單,上文都講過瞭,那麼讓我們來試著用一下吧:
public static void main(String[] args) { ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2); list.add(3); Test.print(list); }
打印的結果如下:
除瞭以上這種寫法,我們還可以將其改成通配符的寫法,先給大傢上代碼:
//?代表通配符 擦除機制 Object public static void print2(ArrayList<?> list) { for (Object t : list) { System.out.println(t); } } }
此處for (Object t : list)
必須這樣寫,因為通配符也是有擦除機制的,會在編譯器編程Object類型。
2. 上界
語法:<? extends 上界>
示例:
public static void printAll(MyArrayList<? extends Number> list) { ... }
代表可以傳入類型實參是 Number 子類的任意類型的 MyArrayList
所以以下調用都是正確的:
printAll(new MyArrayList<Integer>()); printAll(new MyArrayList<Double>()); printAll(new MyArrayList<Number>());
以下調用都是錯誤的:
printAll(new MyArrayList<String>()); printAll(new MyArrayList<Object>());
3. 下界
下界和上界的用法很類似
語法:<? super 下界>
示例:
public static void printAll(MyArrayList<? super Integer> list) { ... }
代表可以傳入類型實參是 Integer 父類的任意類型的 MyArrayList
所以以下調用是正確的:
printAll(new MyArrayList<Integer>()); printAll(new MyArrayList<Number>()); printAll(new MyArrayList<Object>());
以下調用是錯誤的:
printAll(new MyArrayList<String>()); printAll(new MyArrayList<Double>());
六. 泛型的限制
學習完後,我們應該註意泛型使用過程中以下一些限制:
泛型類型參數不支持基本數據類型
無法實例化泛型類型的對象
無法使用泛型類型聲明靜態的屬性
無法使用 instanceof 判斷帶類型參數的泛型類型(因為被擦除機制擦除瞭)
無法創建泛型類數組
無法 create、catch、throw 一個泛型類異常(異常不支持泛型)
泛型類型不是形參一部分,無法重載
好啦,本次泛型知識點的分享就先告一段落瞭,整理不易,但如果能幫到大傢很開心瞭。也希望大傢多理解理解,不論是剛開始學習還是復習,都值得仔細揣摩哦!一起加油吧!
到此這篇關於Java中的紙老虎之泛型的文章就介紹到這瞭,更多相關Java 泛型內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- 一篇文章帶你入門java泛型
- Java集合框架入門之泛型和包裝類
- Java 中泛型 T 和 ? 的區別詳解
- Java中ArrayList與順序表的概念與使用實例
- Java泛型在集合使用與自定義及繼承上的體現和通配符的使用