聊一聊Java 泛型全解
對(duì)于java的泛型我一直屬于一知半解的,平常真心用的不多。直到閱讀《Effect Java》,看到很多平常不了解的用法,才下定決心,需要系統(tǒng)的學(xué)習(xí),并且記錄下來(lái)。
1、泛型的概述:
1.1 泛型的由來(lái)
根據(jù)《Java編程思想》中的描述,泛型出現(xiàn)的動(dòng)機(jī):
有很多原因促成了泛型的出現(xiàn),而最引人注意的一個(gè)原因,就是為了創(chuàng)建容器類。
泛型的思想很早就存在,如C++中的模板(Templates)。模板的精神:參數(shù)化類型
1.2 基本概述
- 泛型的本質(zhì)就是"參數(shù)化類型"。一提到參數(shù),最熟悉的就是定義方法的時(shí)候需要形參,調(diào)用方法的時(shí)候,需要傳遞實(shí)參。那"參數(shù)化類型"就是將原來(lái)具體的類型參數(shù)化
 - 泛型的出現(xiàn)避免了強(qiáng)轉(zhuǎn)的操作,在編譯器完成類型轉(zhuǎn)化,也就避免了運(yùn)行的錯(cuò)誤。
 
1.3 泛型的目的
Java泛型也是一種語(yǔ)法糖,在編譯階段完成類型的轉(zhuǎn)換的工作,避免在運(yùn)行時(shí)強(qiáng)制類型轉(zhuǎn)換而出現(xiàn)ClassCastException,類型轉(zhuǎn)化異常。
1.4 實(shí)例
JDK 1.5時(shí)增加了泛型,在很大的程度上方便在集合上的使用。
不使用泛型:
- public static void main(String[] args) {
 - List list = new ArrayList();
 - list.add(11);
 - list.add("ssss");
 - for (int i = 0; i < list.size(); i++) {
 - System.out.println((String)list.get(i));
 - }
 - }
 
因?yàn)閘ist類型是Object。所以int,String類型的數(shù)據(jù)都是可以放入的,也是都可以取出的。但是上述的代碼,運(yùn)行的時(shí)候就會(huì)拋出類型轉(zhuǎn)化異常,這個(gè)相信大家都能明白。
使用泛型:
- public static void main(String[] args) {
 - List<String> list = new ArrayList();
 - list.add("hahah");
 - list.add("ssss");
 - for (int i = 0; i < list.size(); i++) {
 - System.out.println((String)list.get(i));
 - }
 - }
 
在上述的實(shí)例中,我們只能添加String類型的數(shù)據(jù),否則編譯器會(huì)報(bào)錯(cuò)。
2、泛型的使用
泛型的三種使用方式:泛型類,泛型方法,泛型接口
2.1 泛型類
泛型類概述:把泛型定義在類上
定義格式:
- public class 類名 <泛型類型1,...> {
 - }
 
注意事項(xiàng):泛型類型必須是引用類型(非基本數(shù)據(jù)類型)
2.2 泛型方法
泛型方法概述:把泛型定義在方法上
定義格式:
public <泛型類型> 返回類型 方法名(泛型類型 變量名) { }
注意要點(diǎn):
方法聲明中定義的形參只能在該方法里使用,而接口、類聲明中定義的類型形參則可以在整個(gè)接口、類中使用。當(dāng)調(diào)用fun()方法時(shí),根據(jù)傳入的實(shí)際對(duì)象,編譯器就會(huì)判斷出類型形參T所代表的實(shí)際類型。
- class Demo{
 - public <T> T fun(T t){ // 可以接收任意類型的數(shù)據(jù)
 - return t ; // 直接把參數(shù)返回
 - }
 - };
 - public class GenericsDemo26{
 - public static void main(String args[]){
 - Demo d = new Demo() ; // 實(shí)例化Demo對(duì)象
 - String str = d.fun("湯姆") ; // 傳遞字符串
 - int i = d.fun(30) ; // 傳遞數(shù)字,自動(dòng)裝箱
 - System.out.println(str) ; // 輸出內(nèi)容
 - System.out.println(i) ; // 輸出內(nèi)容
 - }
 - };
 
2.3 泛型接口
泛型接口概述:把泛型定義在接口
定義格式:
- public interface 接口名<泛型類型> {
 - }
 
實(shí)例:
- /**
 - * 泛型接口的定義格式: 修飾符 interface 接口名<數(shù)據(jù)類型> {}
 - */
 - public interface Inter<T> {
 - public abstract void show(T t) ;
 - }
 - /**
 - * 子類是泛型類
 - */
 - public class InterImpl<E> implements Inter<E> {
 - @Override
 - public void show(E t) {
 - System.out.println(t);
 - }
 - }
 - Inter<String> inter = new InterImpl<String>() ;
 - inter.show("hello") ;
 
2.4 源碼中泛型的使用,下面是List接口和ArrayList類的代碼片段。
- //定義接口時(shí)指定了一個(gè)類型形參,該形參名為E
 - public interface List<E> extends Collection<E> {
 - //在該接口里,E可以作為類型使用
 - public E get(int index) {}
 - public void add(E e) {}
 - }
 - //定義類時(shí)指定了一個(gè)類型形參,該形參名為E
 - public class ArrayList<E> extends AbstractList<E> implements List<E> {
 - //在該類里,E可以作為類型使用
 - public void set(E e) {
 - .......................
 - }
 - }
 
2.5 泛型類派生子類
父類派生子類的時(shí)候不能在包含類型形參,需要傳入具體的類型
錯(cuò)誤的方式:
- public class A extends Container {}
 
正確的方式:
- public class A extends Container {}
 
也可以不指定具體的類型,系統(tǒng)就會(huì)把K,V形參當(dāng)成Object類型處理
- public class A extends Container {}
 
2.6 泛型構(gòu)造器
構(gòu)造器也是一種方法,所以也就產(chǎn)生了所謂的泛型構(gòu)造器。
和使用普通方法一樣沒(méi)有區(qū)別,一種是顯示指定泛型參數(shù),另一種是隱式推斷
- public class Person {
 - public <T> Person(T t) {
 - System.out.println(t);
 - }
 - }
 
使用:
- public static void main(String[] args) {
 - new Person(22);// 隱式
 - new <String> Person("hello");//顯示
 - }
 
特殊說(shuō)明:
如果構(gòu)造器是泛型構(gòu)造器,同時(shí)該類也是一個(gè)泛型類的情況下應(yīng)該如何使用泛型構(gòu)造器:因?yàn)榉盒蜆?gòu)造器可以顯式指定自己的類型參數(shù)(需要用到菱形,放在構(gòu)造器之前),而泛型類自己的類型實(shí)參也需要指定(菱形放在構(gòu)造器之后),這就同時(shí)出現(xiàn)了兩個(gè)菱形了,這就會(huì)有一些小問(wèn)題,具體用法再這里總結(jié)一下。 以下面這個(gè)例子為代表
- public class Person<E> {
 - public <T> Person(T t) {
 - System.out.println(t);
 - }
 - }
 
正確用法:
- public static void main(String[] args) {
 - Person<String> person = new Person("sss");
 - }
 
PS:編譯器會(huì)提醒你怎么做的
2.7 高級(jí)通配符
2.7.1背景:
2.7.2 上界通配符
上界通配符顧名思義,表示的是類型的上界【包含自身】,因此通配的參數(shù)化類型可能是T或T的子類。
正因?yàn)闊o(wú)法確定具體的類型是什么,add方法受限(可以添加null,因?yàn)閚ull表示任何類型),但可以從列表中獲取元素后賦值給父類型。如上圖中的第一個(gè)例子,第三個(gè)add()操作會(huì)受限,原因在于List和List是List的子類型。
它表示集合中的所有元素都是Animal類型或者其子類 List
這就是所謂的上限通配符,使用關(guān)鍵字extends來(lái)實(shí)現(xiàn),實(shí)例化時(shí),指定類型實(shí)參只能是extends后類型的子類或其本身。
例如:
這樣就確定集合中元素的類型,雖然不確定具體的類型,但最起碼知道其父類。然后進(jìn)行其他操作。
- 它表示集合中的所有元素都是Animal類型或者其子類
 - List<? extends Animal>
 
2.7.3 下界通配符
下界通配符表示的是參數(shù)化類型是T的超類型(包含自身),層層至上,直至Object
編譯器無(wú)從判斷get()返回的對(duì)象的類型是什么,因此get()方法受限。但是可以進(jìn)行add()方法,add()方法可以添加T類型和T類型的子類型,如第二個(gè)例子中首先添加了一個(gè)Cat類型對(duì)象,然后添加了兩個(gè)Cat子類類型的對(duì)象,這種方法是可行的,但是如果添加一個(gè)Animal類型的對(duì)象,顯然將繼承的關(guān)系弄反了,是不可行的。
它表示集合中的所有元素都是Cat類型或者其父類 List
這就是所謂的下限通配符,使用關(guān)鍵字super來(lái)實(shí)現(xiàn),實(shí)例化時(shí),指定類型實(shí)參只能是extends后類型的子類或其本身
例如
- //Animal是其父類
 - List<? super Cat> list = new ArrayList<Animal>();
 
2.7.4 無(wú)界通配符
任意類型,如果沒(méi)有明確,那么就是Object以及任意的Java類了
無(wú)界通配符用表示,?代表了任何的一種類型,能代表任何一種類型的只有null(Object本身也算是一種類型,但卻不能代表任何一種類型,所以List和List的含義是不同的,前者類型是Object,也就是繼承樹(shù)的最上層,而后者的類型完全是未知的)
3、泛型擦除
3.1 概念
編譯器編譯帶類型說(shuō)明的集合時(shí)會(huì)去掉類型信息
3.2 驗(yàn)證實(shí)例:
- public class GenericTest {
 - public static void main(String[] args) {
 - new GenericTest().testType();
 - }
 - public void testType(){
 - ArrayList<Integer> collection1 = new ArrayList<Integer>();
 - ArrayList<String> collection2= new ArrayList<String>();
 - System.out.println(collection1.getClass()==collection2.getClass());
 - //兩者class類型一樣,即字節(jié)碼一致
 - System.out.println(collection2.getClass().getName());
 - //class均為java.util.ArrayList,并無(wú)實(shí)際類型參數(shù)信息
 - }
 - }
 
輸出結(jié)果:
- true
 - java.util.ArrayList
 
分析:
這是因?yàn)椴还転榉盒偷念愋托螀魅肽囊环N類型實(shí)參,對(duì)于Java來(lái)說(shuō),它們依然被當(dāng)成同一類處理,在內(nèi)存中也只占用一塊內(nèi)存空間。從Java泛型這一概念提出的目的來(lái)看,其只是作用于代碼編譯階段,在編譯過(guò)程中,對(duì)于正確檢驗(yàn)泛型結(jié)果后,會(huì)將泛型的相關(guān)信息擦出,也就是說(shuō),成功編譯過(guò)后的class文件中是不包含任何泛型信息的。泛型信息不會(huì)進(jìn)入到運(yùn)行時(shí)階段。
在靜態(tài)方法、靜態(tài)初始化塊或者靜態(tài)變量的聲明和初始化中不允許使用類型形參。由于系統(tǒng)中并不會(huì)真正生成泛型類,所以instanceof運(yùn)算符后不能使用泛型類
4、泛型與反射
把泛型變量當(dāng)成方法的參數(shù),利用Method類的getGenericParameterTypes方法來(lái)獲取泛型的實(shí)際類型參數(shù)
例子:
- public class GenericTest {
 - public static void main(String[] args) throws Exception {
 - getParamType();
 - }
 - /*利用反射獲取方法參數(shù)的實(shí)際參數(shù)類型*/
 - public static void getParamType() throws NoSuchMethodException{
 - Method method = GenericTest.class.getMethod("applyMap",Map.class);
 - //獲取方法的泛型參數(shù)的類型
 - Type[] types = method.getGenericParameterTypes();
 - System.out.println(types[0]);
 - //參數(shù)化的類型
 - ParameterizedType pType = (ParameterizedType)types[0];
 - //原始類型
 - System.out.println(pType.getRawType());
 - //實(shí)際類型參數(shù)
 - System.out.println(pType.getActualTypeArguments()[0]);
 - System.out.println(pType.getActualTypeArguments()[1]);
 - }
 - /*供測(cè)試參數(shù)類型的方法*/
 - public static void applyMap(Map<Integer,String> map){
 - }
 - }
 
輸出結(jié)果:
- java.util.Map<java.lang.Integer, java.lang.String>
 - interface java.util.Map
 - class java.lang.Integer
 - class java.lang.String
 
通過(guò)反射繞開(kāi)編譯器對(duì)泛型的類型限制
- public static void main(String[] args) throws Exception {
 - //定義一個(gè)包含int的鏈表
 - ArrayList<Integer> al = new ArrayList<Integer>();
 - al.add(1);
 - al.add(2);
 - //獲取鏈表的add方法,注意這里是Object.class,如果寫(xiě)int.class會(huì)拋出NoSuchMethodException異常
 - Method m = al.getClass().getMethod("add", Object.class);
 - //調(diào)用反射中的add方法加入一個(gè)string類型的元素,因?yàn)?/span>add方法的實(shí)際參數(shù)是Object
 - m.invoke(al, "hello");
 - System.out.println(al.get(2));
 - }
 
5 泛型的限制
5.1 模糊性錯(cuò)誤
對(duì)于泛型類User
- public class User<K, V> {
 - public void show(K k) { // 報(bào)錯(cuò)信息:'show(K)' clashes with 'show(V)'; both methods have same erasure
 - }
 - public void show(V t) {
 - }
 - }
 
由于泛型擦除,二者本質(zhì)上都是Obejct類型。方法是一樣的,所以編譯器會(huì)報(bào)錯(cuò)。
換一個(gè)方式:
- public class User<K, V> {
 - public void show(String k) {
 - }
 - public void show(V t) {
 - }
 - }
 
使用結(jié)果:

可以正常的使用5.2 不能實(shí)例化類型參數(shù)
編譯器也不知道該創(chuàng)建那種類型的對(duì)象
- public class User<K, V> {
 - private K key = new K(); // 報(bào)錯(cuò):Type parameter 'K' cannot be instantiated directly
 - }
 
5.3 對(duì)靜態(tài)成員的限制
靜態(tài)方法無(wú)法訪問(wèn)類上定義的泛型;如果靜態(tài)方法操作的類型不確定,必須要將泛型定義在方法上。
如果靜態(tài)方法要使用泛型的話,必須將靜態(tài)方法定義成泛型方法。
- public class User<T> {
 - //錯(cuò)誤
 - private static T t;
 - //錯(cuò)誤
 - public static T getT() {
 - return t;
 - }
 - //正確
 - public static <K> void test(K k) {
 - }
 - }
 
5.4 對(duì)泛型數(shù)組的限制
不能實(shí)例化元素類型為類型參數(shù)的數(shù)組,但是可以將數(shù)組指向類型兼容的數(shù)組的引用
- public class User<T> {
 - private T[] values;
 - public User(T[] values) {
 - //錯(cuò)誤,不能實(shí)例化元素類型為類型參數(shù)的數(shù)組
 - this.values = new T[5];
 - //正確,可以將values 指向類型兼容的數(shù)組的引用
 - this.values = values;
 - }
 - }
 
5.5 對(duì)泛型異常的限制
泛型類不能擴(kuò)展 Throwable,意味著不能創(chuàng)建泛型異常類















 
 
 








 
 
 
 