說明:在學習泛型這一知識點中,主要參考自《瘋狂Java講義》第7章P307-P330的泛型內容,因為是跳著閱讀,所以前面的一些名詞不是特別清楚,這里也做出適當備注,供自己識記與理解。
1.泛型
理解:說到泛型,感覺最初是為了解決Java集合的一個缺點——當我們想要把一個對象放進集合里面的時候,集合就會忘記這個對象的數據類型,再次把它取出來時,它的編譯類型就會變成Object類型(運行類型不會變)。記住我們的目標是:在集合里面存儲不會被忘記數據類型的各種對象。例子:
1 package FanXing; 2 3 public class ListErr { 4 public static void main(String[] args) { 5 List strList = new ArrayList(); 6 strList.add("泛型主題討論"); 7 strList.add(017);//這里不小心把一個Integer對象放在了集合里面,可能報類型強制轉換異常ClassCastException 8 for(int i=0;i<strList.size();i++) 9 {String str = (String)strList.get(i); 10 } 11 } 12 }
編譯信息:
Exception in thread "main" java.lang.Error: Unresolved compilation problems:
List cannot be resolved to a type
ArrayList cannot be resolved to a type
at FanXing.ListErr.main(ListErr.java:5)
圖中的紅色框已經提示我們需要用什么區解決所面臨的的問題了。
為了達到我們的目標,我們想到了可以手動實現編譯時去檢查類型。
例子:(既然會發生異常那我們就在運行前先檢查,我們這里先創建一個對象List,讓它只保存字符串類型,這樣就可以擴展ArrayList類)
package FanXing;import java.util.ArrayList; import java.util.List;class StrList{private List strList = new ArrayList();public boolean add(String ele)//定義StrList的add方法,只添加字符串 {return strList.add(ele);}public String get (int index){return (String)strList.get(index);}public int size(){return strList.size();} }public class ListErr {public static void main(String[] args) {StrList strList = new StrList();strList.add("泛型主題討論");//strList.add(017);如果沒有這一句,代碼可以成功被編譯,否組會報錯。 System.out.println(strList);for(int i=0;i<strList.size();i++){String str = strList.get(i);}} }
上面的代碼中我們定義的StrList類實現了編譯時的異常檢查,當編譯到strList.add(017);時,程序試圖將一個Integer對象加入到StrList集合中,程序在這里會無法編譯通過,因為StrList只接受String的對象。
不過,既然只接受String對象的時候可以編譯通過,說明這個方法還是有用的,但是,這種方法雖然有效,局限性卻非常明顯——我們需要去定義大量的List子類。
雖然這樣也可以實現我們的目標:在集合里面存儲不會被忘記數據類型的各種對象。不過這樣非常非常麻煩,這個時候,我們的泛型就被設計出來了,有了它,我們的目標可以輕易實現。
package FanXing;import java.util.ArrayList; import java.util.List;public class ListErr {public static void main(String[] args) {List<String> strList = new ArrayList<String>();//創建一個List集合,只保留字符串strList.add("泛型主題討論");for(int i = 0;i<strList.size();i++){String str = strList.get(i);}} }
很顯然這樣代碼簡化了很多,List<String>說明這是一個String類型的List。
所以這里我們可以歸納出,如果List<>尖括號里面是其他類型的話也是同理,即有了一個JDK1.5以后引入的概念:
Java泛型(generics)【Java的參數化類型】 :是JDK 5中引入的一個新特性,允許在定義類和接口的時候使用類型參數(type parameter)。聲明的類型參數在使用時用具體的類型來替換。泛型最主要的應用是在JDK 5中的新集合類框架中。
泛型最大的好處是可以提高代碼的復用性。以List接口為例,我們可以將String、Integer等類型放入List中,如不用泛型,存放String類型要寫一個List接口,存放Integer要寫另外一個List接口,泛型可以很好的解決這個問題。
2.深入泛型:
①定義泛型接口、類
public interface List<E>//定義接口,指定形參E,在這個接口里面E可以作為泛型使用 {void add(E,x);Iterator<E> iterator();//A... } public interface Iterator<E>//在這個接口里,E可以作為類型使用 {E next();boolean hasNext();... } public interface Map<K,V>//K,V可以作為類型使用 {Set<K> keySet()//B V put (K key ,V value) ... }
可以發現:在A、B處方法聲明返回值類型是Iterrator<E>和Set<K>,說明他們是一種特殊的數據類型,可以認為是Iterrator和Set類型的子類。
例如:使用List類型的時候,為E形參傳入String實參,則產生了一個新的類型List<String>,把它想象成E全部被String取代的特殊的List子接口。
public interface ListString extends List {void add(String x);Iterator<String> iterator();... }
這樣雖然只是設置了一個List<E>接口,實際實驗時卻是可以產生無數多個List 子接口。
【注意】包含泛型聲明的類型可以在定義變量、創建對象時傳入一個類型實參,從而可以動態的生成無數多個邏輯子表,但這種子類在物理上并不存在。也就是說,List<String>不會被替代成ListString,系統并沒有進行源代碼復制。
②從泛型類派生子類
當定義完泛型接口和泛型父類額時候,我們就可以為接口創建實現類或者從父類派生出子類,不過使用接口和父類的時候,不能再包含類型參數。
//錯誤演示public class A extends List<E>{//A繼承List,List不能跟類型形參 } //正確演示1 public class A extends List<String>{//A繼承List,為List的E形參傳入String } //正確演示2 public class A extends List{//A繼承List,也可以不為類型形參傳入實際的類型參數,不過可能會出現unchecked警告 }
③并不存在泛型類
前面有提到,可以把List<String>類當成是List的子類,這里可能會給大家帶來誤解,實際上,系統并沒有為List<E>生成新的class文件,而且也不會把它當成新的類來處理。這里給一個驗證:
package FanXing;import java.util.ArrayList; import java.util.List;public class ListErr {public static void main(String[] args) {List<String> aaa = new ArrayList<>();List<Integer> aaa1 = new ArrayList<>();System.out.println(aaa1.getClass()==aaa1.getClass());} }
從輸出true可以看出,不管為泛型的類型形參傳入哪一種類型實參,對于Java來說,他們依然被當做同一個類來處理,在內存中也只占用一塊內存空間。
3.類型通配符
package FanXing;import java.util.List;public class test {public void test1 (List c){for(int i = 0;i<c.size();i++){System.out.println(c.get(i));}} }
?這是一個普通的遍歷List集合的代碼,在編譯過程中出現了一個泛型警告,因為在這里使用List接口時沒有傳入實際的參數類型。
修改后:
package FanXing;import java.util.List;public class test {public void test1 (List<?> c){for(int i = 0;i<c.size();i++){System.out.println(c.get(i));}} }
看到原來的List變成了List<?>,這里就引入了類型通配符的概念。
類型通配符就是一個“?”,它的元素類型可以匹配任何類型。
比如,當使用List<?>時,List就成了任何泛型List的父類,比如List既是List<String>的父類,又是List<Integer>的父類,但是,類型之間沒有繼承關系,String是Object的子類,List<String>不是List<Object>的子類。
①設置類型通配符的上限
格式:List<? extends XXX>它表示所有XXX泛型List的父類
②設定類型形參的上限
例子:
public class List<T extends Number & java.io.Serializable> {...//表明T類型必須是Number類或者其子類,并且必須實現java.io.Serializable接口 }
注意:為類型參數指定多個上限時,所有的接口上限必須位于類上限之后。
4.泛型方法
格式:
修飾符 <T,S> 返回值類型 方法名 (形參列表) {//方法體 }
泛型方法和類型通配符的區別:
①大多數時候都可以用泛型方法來替換通配符
②使用通配符情況:用來支持靈活的子類化
③泛型方法允許類型形參被用來表示方法的一個或多個參數之間的類型依賴關系,或者方法返回值與參數之間的類型依賴關系。如果沒有這樣的類型依賴關系,就不應該使用泛型方法。
④形參a的類型或返回值的類型依賴于另一個形參b的類型,則b的類型聲明不應該使用通配符,因為使用通配符表示類型b不確定,那么a的類型也不能確定,這時候要考慮使用泛型方法。
⑤類型不被依賴時,使用通配符。
泛型方法與方法重載:
public class MyUtils {// (1)public static <T> void copy(Collection<T> dest, Collection<? extends T> src) {...}// (2)public static <T> T copy(Collection<? super T> dest, Collection<T> src) {...}public static void main(String[] args) {List<Number> ln = new ArrayList<>();List<Integer> li = new ArrayList<>();copy(ln, li); // 這里會編譯報錯 } }
允許根據方法參數泛型不同進行方法重載,但是調用時,如果編譯器分不清該調用哪個方法則編譯報錯,上面代碼中有兩個copy方法,調用的時候,編譯器既可以調用第一個copy ,也可以調用第二個copy,這樣它就無法確定調用哪個,就會引起編譯報錯。
5.擦除和轉換
?Java中的泛型基本上都是在編譯器這個層次來實現的。在生成的Java字節代碼中是不包含泛型中的類型信息的。使用泛型的時候加上的類型參數,會被編譯器在編譯的時候去掉。這個過程就稱為類型擦除。如在代碼中定義的List<Object>和List<String>等類型,在編譯之后都會變成List。JVM看到的只是List,而由泛型附加的類型信息對JVM來說是不可見的。Java編譯器會在編譯時盡可能的發現可能出錯的地方,但是仍然無法避免在運行時刻出現類型轉換異常的情況。類型擦除也是Java的泛型實現方式與C++模板機制實現方式之間的重要區別。
參考鏈接:Java泛型使用詳解
【備注1】:Java的編譯類型和運行類型的理解。
Java的編譯時類型是由聲明變量時使用的類型決定,運行時類型是由實際賦值的對象所決定。參考鏈接:Java的編譯類型和運行類型
【備注2】:Java泛型中K T V E ? 分別代表的含義:
E – Element (在集合中使用,因為集合中存放的是元素)
T – Type(Java 類)
K – Key(鍵)
V – Value(值)
N – Number(數值類型)
? – 表示不確定的java類型(無限制通配符類型)參考鏈接:泛型相關
【備注3】:為什么說在靜態方法、靜態初始化塊或者靜態變量的聲明和初始化中不允許使用類型參數。
初步理解:
因為泛型是要在對象創建的時候才知道是什么類型的,而對象創建的代碼執行先后順序是static的部分,然后才是構造函數等等。所以在對象初始化之前static的部分已經執行了,如果你在靜態部分引用的泛型,那么毫無疑問虛擬機根本不知道是什么東西,因為這個時候類還沒有初始化。因此在靜態方法、數據域或初始化語句中,為了類而引用泛型類型參數是非法的。
?實際原因:
靜態變量是被泛型類所有實例所共享的。對于聲明為MyClass<T>的類,訪問其中的靜態變量的方法仍然是MyClass.myStaticVar。不管是通過new MyClass<String>還是new MyClass<Integer>創建的對象,都是共享一個靜態變量。假設允許類型參數作為靜態變量的類型。那么考慮下面一種情況:
MyClass<String> class1 = new MyClass<String>();MyClass<Integer> class2 = new MyClass<Integer>();class1.myStaticVar = "hello";class2.myStaticVar = 5;
由于泛型系統的類型擦除(type erasure)。myStaticVar被還原成Object類型,然后當調用class1.myStaticVar= "hello"; 編譯器進行強制類型轉換,即myStaticVar = (String)"hello";接著調用class2.myStaticVar語句時,編譯器繼續進行強制類型轉換,myStaticVar = (Integer)Integer.valueOf(5); 此時myStaticVar是String類型的,當然該語句會在運行時拋出ClassCastException異常,這樣一來存在類型安全問題。因此泛型系統不允許類的靜態變量用類型參數作為變量類型。
當然,靜態泛型方法也不允許。
參考鏈接:
有關靜態不允許使用類型參數的討論
為什么類型參數不能作為靜態變量的類型
【備注4】:Java泛型中上下界限定符extends 和 super的理解:
<? extends T>表示類型的上界,表示參數化類型可能是T或者T的子類;
<? super T>表示類型的下界,表示參數化類型是此類型的超類型(父類型),直至Object。
PECS原則:
如果要從集合中讀取類型T的數據,并且不能寫入,可以使用 ? extends 通配符;(Producer Extends)
如果要從集合中寫入類型T的數據,并且不需要讀取,可以使用 ? super 通配符;(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符。
【備注5】:異常類
異常類一般分為兩種:異常(Exception)和錯誤(Error)
Exception就用try&catch&finally來處理,先在try中運行代碼,catch處理可能出現異常,finally是一定會執行到里面的代碼。 常見的異常有: NumberFormatException(數字格式異常) IndexOutOfBoundsException(數組越界異常) ArithmeticException(除零異常) RuntimeException(運行時異常) 異常常用的方法: getMessage():返回異常的詳細描述字符串。 printStackTrace():跟蹤棧詳細輸出到標準錯誤輸出 printStackTrace(PrintStream s):跟蹤棧詳細輸出到標準錯誤輸出到指定的輸出流 getStackTrace():返回異常的跟蹤棧信息。
Error一般是由虛擬機造成的系統崩潰的。
下一步學習拓展及計劃:
1.總結討論并做成思維導圖
2.理解學習中一直提到的異常這一章的內容
3.擦除的實例(自己嘗試用一個例子實踐)