一、序言
變化一:
在引入范型之前,Java中的類型分為原始類型、復雜類型,其中復雜類型分為數組和類;引入范型後,一個復雜類型可以細分成更多的類型。
例如,原先的List類型,現在細分成List<Object>, List<String>等更多的類型。
注:List<Object>和List<String>是兩種不同的類型, 它們之間沒有繼承關系,即使String繼承了Object。下述代碼是非法的:
List<String> ls = new ArrayList<String>(); List<Object> lo = ls;
這樣設計的原因:根據lo的聲明,編譯器允許你向lo中添加任意對象(例如Integer),但是為其賦值List<String>類型的值,破壞了數據類型的完整性。
變化二:
在引入范型之前,要讓類中的方法支持多個數據類型,就需要對方法進行重載;在引入范型之後,可以更進一步定義多個參數以及返回值之間的關系。
例如,public void write(Integer i, Integer[] ia);及public void write(Double d, Double[] da);的范型版本為:public <T> void write(T t, T[] ta);
二、詳述Java泛型
在細說Java泛型之前,大家可以先看一下如下這樣一段代碼,相信大家肯定不會感覺陌生:
package com.soft; public class WW { public <T> void write(T t, T[] ta){ } }
在編碼時,可經常見到類似於上面這樣的代碼,接下來將詳述Java泛型。
那麼我們首先要知道什麼是泛型,泛型即“參數化類型”,顧名思義,就是將類型由原來具體的類型變為參數化的形式。在定義泛型接口、泛型類和泛型方法的過程中,我們常見的T、E、S等形式的參數用於表示泛型形參,用於接收來自外部使用時候傳入的類型實參。
A、自定義泛型類和泛型方法
(1)定義帶類型參數的類
package com.soft; public class MM<T, S extends T> { public T say(){ T t=null; return t; } }
規則:在定義帶類型參數的類時,在緊跟類命之後的<>內,指定一個或多個類型參數的名字,同時也可以對類型參數的取值范圍進行限定,多個類型參數之間用","進行分隔。
說明:定義完類型參數後,可以在類中定義位置之後的幾乎任意地方使用類型參數(靜態塊,靜態屬性,靜態方法除外),就像使用普通的類型一樣。
注意:父類定義的類型參數不能被子類繼承。
(2)定義帶類型參數方法
package com.soft; public class WW { public <T> void write(T t, T[] ta){ } public <S> void read(S s, S[] sa){ } public <T, S extends T> T test(T t, S s){ S ss=null;
return ss; } }
規則:在定義帶類型參數的方法時,在緊跟可見范圍修飾(如public)之後的<>內,指定一個或多個類型參數的名字,同時也可以對類型參數的取值范圍進行限定,多個類型參數之間用","進行分隔。
說明: 定義完帶類型參數的方法後,可以在方法中定義位置之後的任意地方使用類型參數,就像使用普通的類型一樣。
注意:定義帶類型參數的方法,其主要目的是為了表達多個參數以及返回值之間的關系。例如本例中T和S為繼承關系,返回值的類型(T)和第一個類型參數(t)的類型(T)相同。
補充:
當我們需要一個在邏輯上可以用來表示同時是List<Integer>和List<Number>的類型時,類型通配符應運而生。類型通配符一般是使用"?"代替具體的類型實參。
如果僅僅是想實現多態,請優先使用通配符解決。
初始代碼如下:
public <T> void test1(List<T> u){ ... }
public <S> void test2(List<S> u){ ... }
使用通配符的代碼如下:
public void test(List<?> s){ ... }
B、泛型類和泛型方法的使用
(1)對帶類型參數的類進行賦值
對帶類型參數的類進行賦值有兩種方式:
A、聲明類變量或者實例化時,如下:
List<String> list; list = new ArrayList<String>();
如下賦值方式會報錯:
List<Integer> a = new ArrayList<Integer>(712); List<Number> b = a;
List<Number>不能視為List<Integer>的父類,請參照前面“序言->變化一”中的描述。
B、繼承類或者實現接口時,如下:
public class MyList<E> extends ArrayList<E> implements List<E> {...}
(2)對帶類型參數的方法進行賦值
當調用泛型方法時,編譯器自動對類型參數進行賦值,當不能成功賦值時報編譯錯誤,如下:
package com.soft; import java.util.List; public class WW {public <T, S extends T> T test(T t, S s){ return s; } public <T> T test1(T t, List<T> list){ return t; } public <T> T test2(List<T> list1, List<T> list2){ return null; } public void common(){ Object o = null; Integer i = null; Number n = null; test(n, i);//此時T為Number, S為Integer test(o, i);//T為Object, S為Integer List<Integer> list1 = null; List<Number> list2 = null; test1(i, list1);//此時T為Integer,i為Integer類型,list1為List<Integer>類型 test1(i, list2);//此時T為Number,i為Integer類型,list2為List<Number>類型 test2(list1, list2);//編譯報錯 } }
請讀者自行分析上述代碼中著色語句為什麼會編譯報錯,很簡單的哦O(∩_∩)O哈哈~
(3)對通配符類型參數進行賦值
在上面兩小節中,對是類型參數賦予具體的值,除此,還可以對類型參數賦予不確定值
假設有如下定義:
List<?> unknownList; List<? extends Number> unknownNumberList; List<? super Integer> unknownBaseLineIntgerList;
現對上述定義的變量賦值,如下:
List<String> listString = null; unknownList = listString;
三、類型通配符上限與下限
有時候,我們會聽到類型通配符上限和類型通配符下限這樣的說法,其實在前面的例子中有下面這樣一小段代碼,在該代碼中已經體現出一些上下限的思想
public <T, S extends T> T test(T t, S s){ S ss=null; return ss; }
此處我們進一步說明
package com.soft; public class GenericTest { public static void main(String[] args) { Student<String> name = new Student<String>("xiaoming"); Student<Integer> age = new Student<Integer>(24); Student<Number> number = new Student<Number>(1001); getData(name); getData(age); getData(number); getUpperNumberData(name);//編譯報錯 getUpperNumberData(age); getUpperNumberData(number); } public static void getData(Student<?> data) { System.out.println("data :" + data.getData()); } public static void getUpperNumberData(Student<? extends Number> data){//此處限定類型實參只能是Number類及其子類 System.out.println("data :" + data.getData()); } } class Student<T> { private T data; public Student() { } public Student(T data) { this.data = data; } public T getData() { return data; } }
我想大家通過上例肯定能體會到什麼是類型通配符上限,類型通配符上限通過形如Student<? extends Number>形式定義,相對應的,類型通配符下限為Student<? super Number>形式,其含義與類型通配符上限正好相反,在此不作過多闡述了。
四、注意事項
A、當對類或方法的類型參數進行賦值時,要求對所有的類型參數進行賦值,否則,將得到一個編譯錯誤。
B、在Java集合框架中,對於參數值是未知類型的容器類,只能讀取其中的元素,不能向其中添加元素,因為其類型是未知,編譯器無法識別添加元素的類型和容器的類型是否兼容,唯一的例外是NULL
C、可以使用帶泛型參數的類聲明數組,卻不可以用於創建數組
List<Integer>[] iListArray; iListArray=new ArrayList[10]; iListArray=new ArrayList<Integer>[10];//編譯時錯誤
五、實現原理
(1)一個泛型類被其所有的實例共享
現有如下這樣一段代碼,其打印的結果是什麼?
List<String> l1 = new ArrayList<String>(); List<Integer> l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass());
或許你會說是false,but,you are wrong!它打印出true。我們可以發現雖然傳入的類型實參不同,但其生成的所有對象實例的類型是一樣的,即一個泛型類的所有實例在運行時具有相同的運行時類(class), 而不管他們的實際類型參數。
事實上,泛型之所以叫泛型,就是因為它對其所有可能的類型參數有同樣的行為。類的靜態變量和靜態方法在所有的實例間共享,這就是為什麼在靜態變量的聲明和初始化時或者在靜態方法或靜態初始化代碼中使用類型參數是不合法的原因,類型參數是屬於具體實例的。
(2)擦除(erasure)
在使用泛型類時,雖然傳入了不同的泛型實參,但並沒有真正意義上生成不同的類型,可以接收不同泛型實參的泛型類在內存中只有一個,是原來的最基本的類型(上例中為List),當然,在邏輯上我們可以理解成多個不同的泛型類型。
究其原因,Java泛型是通過java編譯器的稱為擦除(erasure)的前端處理來實現的,Java中的泛型只作用於代碼編譯階段,在編譯過程中正確檢驗泛型結果後,會將泛型的相關信息擦除(erasure),也就是說,成功編譯之後的.class文件中是不包含任何泛型信息的,泛型信息不會進入運行時階段。你可以把它認為是(基本上就是)一個從源碼到源碼的轉換,它把泛型版本轉換成非泛型版本。
基本上,擦除(erasure)去掉了所有的泛型類型信息,所有在尖括號之間的類型信息都被扔掉了,比如說,一個 List<String>類型被轉換為List,所有對類型變量的引用被替換成類型變量的上限(通常是Object)。對此總結成一句話:泛型類型在邏輯上可以看成是多個不同的類型,但實際上都是相同的基本類型。
(3)類型轉換
類型參數在運行時並不存在,這意味著它們不會添加任何時間或者空間上的負擔,這很好,不幸的是,這也意味著你不能依靠它們進行類型轉換,下面這段代碼有如下圖所示的報錯信息:
public <T> T badCast(T t, Object o) { return (T) o; // unchecked warning }
類似的,如下的類型轉換將得到一個unchecked warning,因為運行時環境不會為你做這樣的檢查。
Collection cs = new ArrayList<String>(); Collection<String> cstr = (Collection<String>) cs;
(4)instanceof
泛型類被其所有實例(instances)共享的另一個暗示就是檢查一個實例是不是某一個特定類型的泛型類是沒有意義的,如下圖所示:
六、Class泛型的使用
Java5中的一個變化是類java.lang.Class是泛型化的(Class<T>),這是把泛型擴展到容器類之外的一個很有意思的例子。 你很可能會問,Class的類型參數T代表什麼?它代表Class對象代表的類型,比如說,Class對象String.class的類型為Class<String>,Class對象Serializable.class的類型為Class<Serializable>,此處T代表什麼請自行體會。
Java5為什麼會有這樣的變化,這可以被用來提高你反射代碼的類型安全。特別的,因Class的newInstance()方法返回一個T,你可以在使用反射創建對象時得到更精確的類型。比如說,假定你要寫一個工具方法來進行數據庫查詢,給定一個SQL語句,請返回一個數據庫中符合查詢條件的對象集合(collection),你該怎麼做?
解決方案:
(1)定義接口:
interface Factory<T> { public T[] make(); }
(2)定義查詢方法:
public <T> Collection<T> select(Factory<T> factory, String statement) { Collection<T> result = new ArrayList<T>(); /* run sql query using jdbc */ for ( int i=0; i<10; i++ ) { /* iterate over jdbc results */ T item = factory.make(); /* use reflection and set all of item’s fields from sql results */ result.add( item ); } return result; }
(3)調用(匿名類方案 ):
select(new Factory<EmpInfo>(){ public EmpInfo make() { return new EmpInfo(); } } , ”selection string”);
除了上述方式外,也可以聲明一個類來支持Factory接口:
class EmpInfoFactory implements Factory<EmpInfo> { ... public EmpInfo make() { return new EmpInfo();} } select(getMyEmpInfoFactory(), "selection string");//調用語句,其中getMyEmpInfoFactory()方法返回自定義的支持Factory接口的類
解說:上述兩種解決方案雖然可行,卻很不自然,其缺點是它們需要下面的二者之一:
a.為每個要使用的類型定義冗長的匿名工廠類;
b.為每個要使用的類型聲明一個工廠類並傳遞其對象給調用處
此時可以選擇使用class類型參數,它可以被反射使用,沒有泛型的代碼可能如下:
Collection emps = sqlUtility.select(EmpInfo.class, ”select * from emps”); public static Collection select(Class c, String sqlStatement) { Collection result = new ArrayList(); /* run sql query using jdbc */ for ( /* iterate over jdbc results */ ) { Object item = c.newInstance(); /* use reflection and set all of item’s fields from sql results */ result.add(item); } return result; }
上述方案雖然能達到目的,卻不能給我們返回一個精確類型的集合,因Java5及以後版本中Class是泛型的,我們可以按下面這種方式寫:
Collection<EmpInfo> emps=sqlUtility.select(EmpInfo.class, ”select * from emps”); public static <T> Collection<T> select(Class<T>c, String sqlStatement) { Collection<T> result = new ArrayList<T>(); /* run sql query using jdbc */ for ( /* iterate over jdbc results */ ) { T item = c.newInstance(); /* use reflection and set all of item’s fields from sql results */ result.add(item); } return result; }
上述方案通過一種類型安全的方式得到了我們想要的集合,這項技術是一個非常有用的技巧。