小白學Java:老師!泛型我懂了!
目錄
- 小白學Java:老師!泛型我懂了!
- 泛型概述
- 定義泛型
- 泛型類的定義
- 泛型方法的定義
- 型別變數的限定
- 原生型別與向後相容
- 通配泛型
- 非受限通配
- 受限通配
- 下限通配
- 泛型的擦除和限制
- 型別擦除
- 型別擦除造成的限制
小白學Java:老師!泛型我懂了!
泛型概述
使用泛型機制編寫的程式程式碼要比哪些雜亂地使用Object變數,然後再進行強制型別轉換地程式碼具有更好的安全性和可讀性。
以上摘自《Java核心技術卷一》
在談泛型的定義之前,我先舉一個簡單又真實的例子:如果我想定義一個容器,在容器中放同一類的事物,理所當然嘛。但是在沒有泛型之前,容器中預設儲存的都是Object型別,如果在容器中增加不同型別的元素,都將會被接收,在概念上就不太符合了。關鍵是放進去不同元素之後,會造成一個很嚴重的情況:在取出元素並對裡面的元素進行對應操作的時候,就需要複雜的轉型操作,搞不好還會出錯,就像下面這樣:
//原生型別 ArrayList cats = new ArrayList(); cats.add(new Dog()); cats.add(new Cat()); for (int i = 0; i < cats.size(); i++) { //下面語句型別強轉會發生ClassCastException異常 ((Cat) cats.get(i)).catchMouse(); }
而泛型又是怎麼做的呢?通過尖括號<>
裡的型別引數來指定元素的具體型別。
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new SkyBarking());
dogs.add(new Snoopy());
dogs.add(new Cat());//編譯不通過
//向上轉型,另外兩個是Dog的子類物件
for(Dog d:dogs){
System.out.println(d);
}
至此,泛型優點顯而易見:
- 可讀性:很明顯嘛,一看就知道是存著一組Dog物件。
- 安全性:如果型別不符,編譯不會通過,因此不再需要進行強制轉換。
妙啊,從中我們可以體會泛型的理念:泛型只存在編譯器,寧可讓錯誤發生在編譯期,也不願意讓程式在執行時出現型別轉換異常。 因為bug發生在編譯期更容易去找到並修復。 除此之外:
- 可將子類型別傳入父類物件的容器之中,向上轉型。
- 不必糾結物件的型別,可用增強for迴圈實現遍歷。
定義泛型
再次強調,所謂泛型,即引數化型別,就是隻有在使用類的時候,才把型別確定下來,相當的靈活。
泛型類的定義
class Element<T>{
private T value;
Element(T value){
this.value = value;
}
public T getvalue() {
return this.value;
}
}
- 引入型別變數T(按照規範,也可以有多個,用逗號隔開),並用
<>
擴起,放在類名後面。 - 其實就是可以把T假想成平時熟悉的型別,這裡只不過用個符號代替罷了。
Element<String> element = new Element<>("天喬巴夏");
System.out.println(element.getvalue());
- 使用泛型時,用具體型別(只能是引用型別)替換型別變數T即可。泛型其實可以堪稱普通類的工廠。
- 泛型介面的定義與類定義類似,就暫且不做贅述。
泛型方法的定義
class ParaMethod {
public static <T> T getMiddle(T[] a) {
return a[a.length/2];
}
}
- 注意該方法並不是在泛型類中所定義,而是在普通類中定義的泛型方法。
- 型別變數T放在修飾符的後面,返回型別的前面,只是正好我們這邊返回型別也是T。
int m = ParaMethod.getMiddle(new Integer[]{1,2,3,4,5});
//返回Integer型別,自動拆箱
System.out.println(m);//3
型別變數的限定
我們上面講到,泛型擁有足夠的靈活性,意味著我傳啥型別,執行的時候就是啥型別。但是,實際生活中,我要是想對整數型別進行操作,不想讓其他型別混入,怎麼辦呢?對了,加上型別限定。
public static <T extends Number> T getNum(T num) {
return num;
}
- 定義格式:
修飾符 <T extends 型別上限> 返回型別 方法名 引數列表
,如上表示對型別變數的上限進行限定,只有Number及其子類可以傳入。 - 規定型別的上限的數量最多隻能有一個。
- 既然類的定義是這樣子,那大膽猜測一下,定義介面上線是不是就應該用
implements
關鍵字呢?答案是:否!介面依舊也是extends
。
public static <T extends Comparable & Serializable> T max(T[] a) {
if (a == null || a.length == 0) return null;
T maximum = a[0];
for (int i = 1; i < a.length; i++) {
if (maximum.compareTo(a[i]) < 0) maximum = a[i];
}
return maximum;
}
- 需要注意的是:如果允許多個介面作為上限,介面可以用&隔開。
- 如果規定上限時,介面和類都存在,類需要放在前面,
<T extends 類&介面>
。 - 沒有規定上限的泛型型別可以視為:
<T extends Object>
。
原生型別與向後相容
使用泛型類而不指定具體型別,這樣的泛型型別就叫做原生型別(raw type),用於和早期的Java版本向後相容,畢竟泛型JDK1.5之後才出呢。其實我們在本篇開頭舉的例子就包含著原生型別,ArrayList cats = new ArrayList();
。
ArrayList cats = new ArrayList();//raw type
它大致可以被看成指定泛型型別為Object的型別。
ArrayList<Object> cats = new ArrayList<Object>();
注意:原生型別是不安全的!因為可能會引發型別轉換異常,上面已經提到。所以我們在使用過程中,儘量不要使用原生型別。
通配泛型
我們通過下面幾個例子,來詳細總結通配型別出現的意義,以及具體的用法。
非受限通配
如果我想定義一個方法,讓它接收一個集合,不關注集合中元素的型別,並把集合中的元素打印出來,應該怎麼辦呢?
上面談到泛型,你可能會這樣寫,讓方法接收一個Object的集合,這樣子你傳進來啥我都接,完成之後美滋滋,一除錯就不對了:
public static void print(ArrayList<Object> arrayList){
//錯誤!:arrayList.add(5);
for(int i = 0;i< arrayList.size();i++){
System.out.println(arrayList.get(i));
}
}
ArrayList<Integer> arr = new ArrayList<>();
print(arr);
究其原因:Integer是Object的子類的確沒錯,但是ArrayList
public static void print(ArrayList<?> arrayList)
- 定義格式:
?
表示接收所有的型別,可以看成是? extends Object
,這個就是我們即將要說的受限通配的格式了,非受限通配就是以Object為上限的通配,可不是嘛。 - 使用萬用字元
?
時,由於型別的不確定,你不能夠呼叫與物件型別相關的方法,就像上面的arrayList.add(5);
就是錯誤的。
受限通配
如果我想定義一個方法,讓它接收一個整數型別的集合,應該怎麼辦呢?
public static void operate(ArrayList<Number> list){
/*operate a List of Number*/
}
/* 呼叫方法 */
ArrayList<Integer> arr = new ArrayList<>();
operate(arr);
上面的這個錯誤,想必你不會再犯,因為ArrayList
public static void operate(ArrayList<? extends Number> list){
/*operate a List of Number*/
}
- 形式:
?extends T
,表示T或者T的子型別。
下限通配
說完了上面兩個,第三個我就不賣關子了,直接寫上它的定義格式:? super T
,表示T或者T的父型別。
public static <T> void show(ArrayList<T> arr1,ArrayList<? super T>arr2){
System.out.println(arr1.get(0)+","+arr2.get(0));
}
ArrayList<Number> arr1 = new ArrayList<>();
ArrayList<Integer> arr2 = new ArrayList<>();
//編譯出錯
show(arr1,arr2);
以上將會編譯錯誤,因為限定show方法中第二個引數的型別必須時第一個引數型別或者其父類。
泛型的擦除和限制
型別擦除
- 泛型的相關資訊可被編譯器使用,但是這些資訊在執行時是不可用的。
泛型僅僅存在於編譯,一但編譯器確認泛型型別的安全性,就會將它轉換原生型別。
當編譯泛型類、介面或方法時,編譯器會用Object代替泛型型別。以上面的例子舉例:
Element<String> element = new Element<>("天喬巴夏");
System.out.println(element.getvalue());
將會變成:
Element element = new Element("天喬巴夏");
System.out.println((String)element.getvalue());
- 當一個泛型受限時,編譯器會用其首限型別替換它。
public static void operate(ArrayList<? extends Number> list){
/*operate a List of Number*/
}
將變成下面這樣:
public static void operate(ArrayList<Number> list){
/*operate a List of Number*/
}
- 不管實際的具體型別是什麼,泛型類總是被它的所有例項所共享。
ArrayList<Number> arr1 = new ArrayList<>();
ArrayList<Integer> arr2 = new ArrayList<>();
System.out.println(arr1 instanceof ArrayList);//true
System.out.println(arr2 instanceof ArrayList);//true
可以看到,雖然ArrayList<Number>
和ArrayList<Integer>
是兩種型別,但是由於泛型在編譯器進行型別擦除,它們在執行時會被載入進同一個類,即ArrayList類。所以下面這句將會編譯出錯。
System.out.println(arr1 instanceof ArrayList<Number>);//編譯出錯
型別擦除造成的限制
- 不能使用泛型型別引數建立例項。
T t = new T();//錯誤
- 不能使用泛型型別引數建立陣列。
//錯誤:E[] elements = new E[5];
E[] elements = (E[])new Object[5];
//可以通過型別轉換規避限制,但仍會導致一個unchecked cast警告,編譯器不能夠確保在執行時型別轉換能否成功。
- 不允許使用泛型類建立泛型陣列。
ArrayList<String>[] list = new ArrayList<String>[5];//錯誤
- 上面說到,泛型類的所有例項具有相同的執行時類,所以泛型類的靜態變數和方法時被它的例項們所共享的,所以下面三種做法都不行。
class Test<T> {
public static void m(T o1) {//錯誤
}
public static T o1;//錯誤
static {
T o2;//錯誤
}
}
- 異常類不能是泛型的。因為如果異常類可以是泛型的,那麼想要捕獲異常,JVM需要檢查try子句丟擲的異常是否和catch子句中的異常型別匹配,但是泛型型別擦除的存在,執行時的型別並不能夠知道,所以沒什麼道理。
本文若有敘述不當之處,還望評論區批評指正。
參考資料:
《Java核心結束卷一》、《Java語言程式設計與資料結構》
泛型就這麼簡單
https://www.programcreek.com/category/java-2/generics-jav