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

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