java 泛型全解 - 絕對最詳細

背景

對於java的泛型我一直屬於只知其一;不知其二的,日常真心用的很少。直到閱讀《Effect Java》,看到不少日常不瞭解的用法,才下定決心,須要系統的學習,而且記錄下來。java


一、泛型的概述:

1.1 泛型的由來

根據《Java編程思想》中的描述,泛型出現的動機:git

有不少緣由促成了泛型的出現,而最引人注意的一個緣由,就是爲了建立容器類。
複製代碼

泛型的思想很早就存在,如C++中的模板(Templates)。模板的精神:參數化類型github

1.2 基本概述

  • 泛型的本質就是"參數化類型"。一提到參數,最熟悉的就是定義方法的時候須要形參,調用方法的時候,須要傳遞實參。那"參數化類型"就是將原來具體的類型參數化
  • 泛型的出現避免了強轉的操做,在編譯器完成類型轉化,也就避免了運行的錯誤。

1.3 泛型的目的

  • Java泛型也是一種語法糖,在編譯階段完成類型的轉換的工做,避免在運行時強制類型轉換而出現ClassCastException,類型轉化異常。

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類型的數據都是能夠放入的,也是均可以取出的。可是上述的代碼,運行的時候就會拋出類型轉化異常,這個相信你們都能明白。數組

  • 使用泛型:
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類型的數據,不然編譯器會報錯。bash


二、泛型的使用

泛型的三種使用方式:泛型類泛型方法泛型接口app

2.1 泛型類

  • 泛型類概述:把泛型定義在類上
  • 定義格式:
public class 類名 <泛型類型1,...> {
    
}
複製代碼
  • 注意事項:泛型類型必須是引用類型(非基本數據類型)

2.2 泛型方法

  • 泛型方法概述:把泛型定義在方法上
  • 定義格式:
public <泛型類型> 返回類型 方法名(泛型類型 變量名) {
    
}
複製代碼
  • 注意要點:
    • 方法聲明中定義的形參只能在該方法裏使用,而接口、類聲明中定義的類型形參則能夠在整個接口、類中使用。當調用fun()方法時,根據傳入的實際對象,編譯器就會判斷出類型形參T所表明的實際類型。
class Demo{  
  public <T> T fun(T t){   // 能夠接收任意類型的數據  
   return t ;     // 直接把參數返回  
  }  
};  
public class GenericsDemo26{  
  public static void main(String args[]){  
    Demo d = new Demo() ; // 實例化Demo對象  
    String str = d.fun("湯姆") ; // 傳遞字符串  
    int i = d.fun(30) ;  // 傳遞數字,自動裝箱  
    System.out.println(str) ; // 輸出內容  
    System.out.println(i) ;  // 輸出內容  
  }  
};
複製代碼

2.3 泛型接口

  • 泛型接口概述:把泛型定義在接口
  • 定義格式:
public interface 接口名<泛型類型> {
    
}
複製代碼
  • 實例:
/**
 * 泛型接口的定義格式:        修飾符  interface 接口名<數據類型> {}
 */
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 泛型類派生子類

父類派生子類的時候不能在包含類型形參,須要傳入具體的類型ide

  • 錯誤的方式:

public class A extends Container<K, V> {}學習

  • 正確的方式:

public class A extends Container<Integer, String> {}測試

  • 也能夠不指定具體的類型,系統就會把K,V形參當成Object類型處理

public class A extends Container {}

2.6 泛型構造器

  • 構造器也是一種方法,因此也就產生了所謂的泛型構造器。
  • 和使用普通方法同樣沒有區別,一種是顯示指定泛型參數,另外一種是隱式推斷
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");//顯示
}
複製代碼
  • 特殊說明:

    • 若是構造器是泛型構造器,同時該類也是一個泛型類的狀況下應該如何使用泛型構造器:由於泛型構造器能夠顯式指定本身的類型參數(須要用到菱形,放在構造器以前),而泛型類本身的類型實參也須要指定(菱形放在構造器以後),這就同時出現了兩個菱形了,這就會有一些小問題,具體用法再這裏總結一下。 如下面這個例子爲表明
    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 <? extends T> 上界通配符

  • 上界通配符顧名思義,<? extends T>表示的是類型的上界【包含自身】,所以通配的參數化類型多是T或T的子類。

    • 正由於沒法肯定具體的類型是什麼,add方法受限(能夠添加null,由於null表示任何類型),但能夠從列表中獲取元素後賦值給父類型。如上圖中的第一個例子,第三個add()操做會受限,緣由在於List和List是List<? extends Animal>的子類型。
    它表示集合中的全部元素都是Animal類型或者其子類
     List<? extends Animal>
    複製代碼
  • 這就是所謂的上限通配符,使用關鍵字extends來實現,實例化時,指定類型實參只能是extends後類型的子類或其自己。

    • 例如:
    • 這樣就肯定集合中元素的類型,雖然不肯定具體的類型,但最起碼知道其父類。而後進行其餘操做。
    //Cat是其子類
        List<? extends Animal> list = new ArrayList<Cat>();
    複製代碼

2.7.3 <? super T> 下界通配符

  • 下界通配符<? super T>表示的是參數化類型是T的超類型(包含自身),層層至上,直至Object

    • 編譯器無從判斷get()返回的對象的類型是什麼,所以get()方法受限。可是能夠進行add()方法,add()方法能夠添加T類型和T類型的子類型,如第二個例子中首先添加了一個Cat類型對象,而後添加了兩個Cat子類類型的對象,這種方法是可行的,可是若是添加一個Animal類型的對象,顯然將繼承的關係弄反了,是不可行的。
    它表示集合中的全部元素都是Cat類型或者其父類
     List <? super Cat>   
    複製代碼
  • 這就是所謂的下限通配符,使用關鍵字super來實現,實例化時,指定類型實參只能是extends後類型的子類或其自己

    • 例如
    //Animal是其父類
    List<? super Cat> list = new ArrayList<Animal>();
    複製代碼

2.7.4 <?> 無界通配符

  • 任意類型,若是沒有明確,那麼就是Object以及任意的Java類了
  • 無界通配符用<?>表示,?表明了任何的一種類型,能表明任何一種類型的只有null(Object自己也算是一種類型,但卻不能表明任何一種類型,因此List和List的含義是不一樣的,前者類型是Object,也就是繼承樹的最上層,然後者的類型徹底是未知的)

三、泛型擦除

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類型同樣,即字節碼一致
        
        System.out.println(collection2.getClass().getName());
        //class均爲java.util.ArrayList,並沒有實際類型參數信息
    }
}
複製代碼
  • 輸出結果:
true
java.util.ArrayList
複製代碼
  • 分析:
    • 這是由於無論爲泛型的類型形參傳入哪種類型實參,對於Java來講,它們依然被當成同一類處理,在內存中也只佔用一塊內存空間。從Java泛型這一律念提出的目的來看,其只是做用於代碼編譯階段,在編譯過程當中,對於正確檢驗泛型結果後,會將泛型的相關信息擦出,也就是說,成功編譯事後的class文件中是不包含任何泛型信息的。泛型信息不會進入到運行時階段。
    • 在靜態方法、靜態初始化塊或者靜態變量的聲明和初始化中不容許使用類型形參。因爲系統中並不會真正生成泛型類,因此instanceof運算符後不能使用泛型類

四、泛型與反射

  • 把泛型變量當成方法的參數,利用Method類的getGenericParameterTypes方法來獲取泛型的實際類型參數
  • 例子:
public class GenericTest {

    public static void main(String[] args) throws Exception {
        getParamType();
    }
    
     /*利用反射獲取方法參數的實際參數類型*/
    public static void getParamType() throws NoSuchMethodException{
        Method method = GenericTest.class.getMethod("applyMap",Map.class);
        //獲取方法的泛型參數的類型
        Type[] types = method.getGenericParameterTypes();
        System.out.println(types[0]);
        //參數化的類型
        ParameterizedType pType  = (ParameterizedType)types[0];
        //原始類型
        System.out.println(pType.getRawType());
        //實際類型參數
        System.out.println(pType.getActualTypeArguments()[0]);
        System.out.println(pType.getActualTypeArguments()[1]);
    }

    /*供測試參數類型的方法*/
    public static void applyMap(Map<Integer,String> map){

    }
}
複製代碼
  • 輸出結果:
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);
		//調用反射中的add方法加入一個string類型的元素,由於add方法的實際參數是Object
		m.invoke(al, "hello");
		System.out.println(al.get(2));
	}

複製代碼

5 泛型的限制

5.1 模糊性錯誤

  • 對於泛型類User<K,V>而言,聲明瞭兩個泛型類參數。在類中根據不一樣的類型參數重載show方法。
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) {

    }
}
複製代碼

因爲泛型擦除,兩者本質上都是Obejct類型。方法是同樣的,因此編譯器會報錯。

換一個方式:

public class User<K, V> {

    public void show(String k) {

    }
    public void show(V t) {

    }
}
複製代碼

使用結果:

能夠正常的使用

5.2 不能實例化類型參數

編譯器也不知道該建立那種類型的對象

public class User<K, V> {

    private K key = new K(); // 報錯:Type parameter 'K' cannot be instantiated directly

}
複製代碼

5.3 對靜態成員的限制

靜態方法沒法訪問類上定義的泛型;若是靜態方法操做的類型不肯定,必需要將泛型定義在方法上。

若是靜態方法要使用泛型的話,必須將靜態方法定義成泛型方法

public class User<T> {

    //錯誤
    private static T t;

    //錯誤
    public static T getT() {
        return t;
    }

    //正確
    public static <K> void test(K k) {

    }
}
複製代碼

5.4 對泛型數組的限制

  • 不能實例化元素類型爲類型參數的數組,可是能夠將數組指向類型兼容的數組的引用
public class User<T> {

    private T[] values;

    public User(T[] values) {
        //錯誤,不能實例化元素類型爲類型參數的數組
        this.values = new T[5];
        //正確,能夠將values 指向類型兼容的數組的引用
        this.values = values;
    }
}
複製代碼

5.5 對泛型異常的限制

泛型類不能擴展 Throwable,意味着不能建立泛型異常類 答案連接

參考:

相關文章
相關標籤/搜索