Java:泛型(深入解析,一文讀懂)

基本概念和原理


爲什麼使用泛型:

在沒有使用泛型之前,一旦把一個對象“丟進”Java集合中,集合就會忘記對象的類型,把所有的對象當成Object類型處理。當程序從集合中取出對象後,就需要進行強制類型轉換,這種強制類型轉換不僅是代碼臃腫,而且容易引起ClassCastException異常。

標題的基本概念:

所謂泛型,就是允許在定義類、接口、方法時使用類型形參,這個類型形參(或叫泛型)將在聲明變量、創建對象、調用方法時動態地指定(即傳入實際的類型參數,也可稱爲類型實參)。

使用泛型的好處:
  1. 更好的安全性
    通過使用泛型 ,開發環境和編譯器能確保不會用錯類型,爲程序多設置一道安全防護網。

  2. 更好的可讀性
    使用泛型,可以省去繁瑣的強制類型轉換,再加上明確的類型信息,代碼可讀性也會更好。

泛型的使用:

public class Test<T> {
    T first;
    T second;
    public Test(T first, T second) {
        this.first = first;
        this.second = second;
    }
    public T getFirst() {
        return first;
    }
    public T getSecond() {
        return second;
    }
}

Test是一個泛型類,與普通類的區別:

  • 類名後多了一個<T>;
  • first和second的類型都是T;

T表示類型參數,泛型就是類型參數化,處理的數據類型不是固定的,而是可以作爲參數傳入。

泛型的原理:

Java有Java編譯器和Java虛擬機,編譯器將Java源代碼轉換爲.class文件,虛擬機加載並運行.class文件。對於泛型類,Java編譯器會將泛型代碼轉換爲普通的非泛型代碼,就像上面的普通Test類代碼及其使用代碼一樣,將類型參數T擦除,替換Object,插入必要的強制類型轉換。Java虛擬機實際執行的時候,它是不知道泛型這回事的,只知道普通的類及代碼。

泛型擦除:

Java泛型是通過擦除實現額,類定義中的類型參數如T會被替換爲Object,在程序運行過程中,不知道泛型的實際類型參數,比如Test<Integer>,運行中只知道Test,而不知道Integer。

深入泛型

//定義接口時指定了一個泛型形參,該形參名爲E
public interface Test<E> {
    
    //在接口方法裏,E可作爲類型使用
    //下面方法可以使用E作爲類型參數
    void add(E x);
    Iterable<E> iterator();
    
	//在接口裏,E完全可以作爲類型使用
    E next();
}
public interface Map<K, V> {

    //在接口裏K、V完全可以作爲類型使用
    Set<K> keySet();
    V put(K key, V value);
}

解釋:
允許在定義接口、類時聲明泛型形參,泛型形參在整個接口、類體內可當成類型使用,幾乎所有可使用普通方法類型的地方都可以使用這種泛型形參。

注:
當創建泛型聲明的自定義類,爲該類定義構造器時,構造器名還是原來的類名,不要增加泛型聲明。 例如,爲Test<T>類定義構造器,其構造器名依然是Test,而不是Test<T>;!調用該構造器時卻可以使用Test<T>的形式,當然應該爲T形參傳入實際的類型參數。Java7提供了“菱形”語法,允許省略<>中的類型實參。

從泛型類派生子類:

方法中的形參代表變量、常量、表達式等數據,本文把它們直接稱爲形參,或者稱爲數據形參。定義方式時可以聲明數據形參,調用方法(使用方法)時必須爲這些數據形參傳入實際的數據;於此類似的是,定義類、接口、方法時可以使用聲明泛型形參,使用類、接口、方法時應該爲泛型形參傳入實際的類型。

//定義類A繼承Apple類,Apple類不能跟泛型形參
public class A extends Apple<T> {}   //錯誤

//使用Apple類時爲T形參傳入String類型
public class A extends Apple<String> //正確

調用方法時必須爲所有的數據形參傳入參數值, 與調用方法不同的是,使用類、接口時也可以不爲泛型形參傳入實際的類型參數,即下面代碼也是正確的。

public class A extends Apple  //正確

像這種使用Apple類時省略泛型的形式被稱爲原始類型(raw type)。
如果使用Apple類時沒有傳入實際的類型(即使用原始類型),Java編譯器可能發出警告:使用了未經檢查或不安全的操作 - - 就是泛型檢查的警告。

並不存在泛型類:

看如下代碼:

List<String> list = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
//調用getClass()方法來比較list和list2的類是否相等
System.out.println(list.getClass() == list2.getClass());

運行上面的代碼片段,可能有讀者認爲應該輸出false,但實際輸出true。因爲不管泛型的實際類型參數是什麼,它們在運行時總有同樣的類(Class)。
不管爲泛型形參傳入哪一種類型實參,對於Java來說,它們依然被當成同一個類來處理,在內存中也只佔用一塊內存空間,因此在靜態方法、靜態初始化塊或者靜態變量(它們都是類相關的)的聲明和初始化中不允許使用泛型形參。

public class Test<T> {

    //下面代碼錯誤,不能在靜態變量聲明中使用泛型形參
    static T info;
    
    //正確
    T age;
    public void foo(T msg) { }

    //下面代碼錯誤,不能再靜態方法聲明中使用使用泛型形參
    public static void bar(T msg) {}
}

由於系統中並不會真正生成泛型類,所以instanceof運算符後不能使用泛型類。

java.util.Collection<String> cs = new java.util.ArrayList<String>();
//下面代碼編譯時引起錯誤:instanceof運算符後不能使用泛型
if(cs instanceof java.util.ArrayList<String>) {}
使用類型通配符:

爲了表示各種泛型List的父類,可以使用類型通配符,類型通配符是一個問號(?),將一個問號作爲類型實參傳給List集合,寫作:List<?>(意思是元素類型未知的List)。這個問號(?)被稱爲通配符,它的元素類型可以匹配任何類型。
看如下代碼:

public void test(List<?> c) { }
設定類型通配符的上限(協變):

指定通配符上限的集合,只能從集合中取元素(取出的元素總是上限的類型或其子類),不能向集合中添加元素(因爲編譯器沒法確定集合元素實際是哪種子類型)。

設定類型通配符的下限(逆變):

除可以指定通配符的上限之外,Java也允許指定通配符的下限,通配符的下限用<? super 類型>的方式類指定,通配符下限的作用與通配符上限的作用恰好相反。
Foo是Bar的子類,當程序需要一個A<? super Foo>變量時,程序可以將A<Bar>、A<Object>賦值給A<? super Foo>類型的變量,這種方式稱爲逆變。
對於逆變的泛型來說,編譯器只知道集合元素是下限的父類型,但具體是哪種父類型則不確定。因此,這種逆變的泛型集合能向其中添加元素(因爲實際賦值的集合元素總是逆變聲明的父類),從集合中取元素時只能被當成Object類型處理(編譯器無法確定取出的到底是哪個父類的對象)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章