1. 程式人生 > 程式設計 >java 泛型全解 - 絕對最詳細

java 泛型全解 - 絕對最詳細

背景

對於java的泛型我一直屬於一知半解的,平常真心用的不多。直到閱讀《Effect Java》,看到很多平常不瞭解的用法,才下定決心,需要系統的學習,並且記錄下來。


1、泛型的概述:

1.1 泛型的由來

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

有很多原因促成了泛型的出現,而最引人注意的一個原因,就是為了建立容器類。
複製程式碼

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

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型別的資料,否則編譯器會報錯。


2、泛型的使用

泛型的三種使用方式:泛型類泛型方法泛型介面

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 泛型類派生子類

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

  • 錯誤的方式:

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、泛型擦除

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運運算元後不能使用泛型類

4、泛型與反射

  • 把泛型變數當成方法的引數,利用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,意味著不能建立泛型異常類 答案連結

參考: