小白學Java:泛型詳解
泛型概述
使用泛型機制編寫的程序代碼要比哪些雜亂地使用Object變量,而後再進行強制類型轉換地代碼具備更好的安全性和可讀性。css
以上摘自《Java核心技術卷一》html
在談泛型 的定義以前,我先舉一個簡單又真實的例子:若是我想定義一個容器,在容器中放同一類的事物,理所固然嘛。可是在沒有泛型以前,容器中默認存儲的都是Object類型,若是在容器中增長不一樣類型的元素,都將會被接收,在概念上就不太符合了。關鍵是放進去不一樣元素以後,會形成一個很嚴重的狀況:在取出元素並對裏面的元素進行對應操做的時候,就須要複雜的轉型操做,搞很差還會出錯,就像下面這樣:java
//原生類型
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();
}
而泛型又是怎麼作的呢?經過尖括號<>
裏的類型參數來指定元素的具體類型。segmentfault
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循環實現遍歷。
定義泛型
再次強調,所謂泛型,即參數化類型 ,就是隻有在使用類的時候,才把類型肯定下來,至關的靈活。markdown
泛型類的定義
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();
。async
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
並非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
並非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方法中第二個參數的類型必須時第一個參數類型或者其父類。
泛型的擦除和限制 類型擦除
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-java-2/
posted @
2020-01-13 23:50
天喬巴夏丶 閱讀(
... ) 評論(
)
編輯
收藏