Java泛型總結---基本用法,類型限定,通配符,類型擦除

1、基本概念和用法

在Java語言處於尚未出現泛型的版本時,只能經過Object是全部類型的父類和類型強制轉換兩個特色的配合來實現類型泛化。例如在哈希表的存取中,JDK1.5以前使用HashMap的get()方法,返回值就是一個Object對象,因爲Java語言裏面全部的類型都繼承於java.lang.Object,那Object轉型爲任何對象成都是有可能的。可是也由於有無限的可能性,就只有程序員和運行期的虛擬機才知道這個Object究竟是個什麼類型的對象。在編譯期間,編譯器沒法檢查這個Object的強制轉型是否成功,若是僅僅依賴程序員去保障這項操做的正確性,許多ClassCastException的風險就會被轉嫁到程序運行期之中。java

泛型是JDK1.5的一項新特性,它的本質是將類型參數化,簡單的說就是將所操做的數據類型指定爲一個參數,在用到的時候經過傳參來指定具體的類型。在Java中,這種參數類型能夠用在類、接口和方法的建立中,分別稱爲泛型類、泛型接口和泛型方法。程序員

一個泛型類的例子以下:segmentfault

//將要操做的數據類型指定爲參數T
public class Box<T> {
    private T t;
    
    public void add(T t) {
        this.t = t;
    }
    
    public T get() {
        return this.t;
    }
}
//使用的時候指定具體的類型爲Integer
//那麼Box類裏面的全部T都至關於Integer了
Box<Integer> integerBox = new Box<Integer>();

泛型接口和泛型方法的定義和使用示例以下:數組

//泛型接口
interface Show<T,U> {
    void show(T t,U u);
}

class ShowTest implements Show<String,Date> {
    @Override  
    public void show(String str,Date date) {
        System.out.println(str);
        System.out.println(date);
    }
}

public static void main(String[] args) {  
    ShowTest showTest = new ShowTest();
    showTest.show("Hello",new Date());
}
//泛型方法
public <T, U> T get(T t, U u) {
    if (u != null)
        return t;
    else
        return null;
}

String str = get("Hello", "World");

從上面的例子能夠看出,用尖括號<>來聲明泛型變量,能夠有多個類型變量,例如Show<T, U>,可是類型變量名不能重複,例如Show<T, T>是錯誤的。另外,類型變量名通常使用大寫形式,且比較短(不強制,只是一種命名規約),下面是一些經常使用的類型變量:框架

  • E:元素(Element),多用於java集合框架
  • K:關鍵字(Key)
  • N:數字(Number)
  • T:類型(Type)
  • V:值(Value)
  • S:第二類型
  • U:第三類型

2、泛型變量的類型限定

類型限定就是使用extends關鍵字對類型變量加以約束。好比限定泛型參數只接受Number類或者子類Integer、Float等,能夠這樣限定 ,這樣限定以後,實際參數只能是Number類或者Number的子類。下面舉例詳細說明: ide

//定義一個水果類
//裏面有一個示例方法getWeight()能夠獲取水果重量
public class Fruit {
    public int getWeight() {
        return 10; //這裏假設全部水果重量都是10
    }
}
public class Apple extends Fruit {}

--------------------------------------------------------------------------------------------

//定義泛型類Box,並限定類型參數爲Fruit
public class Box<T extends Fruit> {}

--------------------------------------------------------------------------------------------

//因爲Box限定了類型參數,實際類型參數只能是Fruit或者Fruit的子類
Box<Fruit> integerBox = new Box<Fruit>();//編譯經過
Box<Apple> integerBox = new Box<Apple>();//編譯經過
Box<Integer> integerBox = new Box<Integer>();//編譯器報錯

上面代碼用虛線分爲三個部分,第一個部分是舉例用的,定義一個水果類Fruit和它的子類Apple;第二部分定義一個泛型類Box,而且限定了泛型參數爲Fruit,限定以後,實際類型只能是Fruit或者Fruit的子類,因此第三部分,實際泛型參數是Integer就會報錯。學習

經過限定,箱子Box就只能裝水果了,這是有好處的,舉個例子,好比Box裏面有一個getBigFruit()方法能夠比較兩個水果大小,而後返回大的水果,代碼以下:ui

public class Box<T extends Fruit>{

    public T getBigFruit(T t1, T t2) {
    
        // if (!(t1 instanceof Fruit) || !(t2 instanceof Fruit)) {
        //    throw new RuntimeException("T不是水果");
        // }
        
        if (t1.getWeight() > t2.getWeight()) {
            return t1;
        }
        return t2;
    }
}

代碼中須要注意兩個地方:一個是註釋的三行,參數限定以後,不必判斷t1和t2的類型了,若是類型不對,在Box實例化的時候就報錯了;另外一個是t1.getWeight(),在Box類裏面,t1是T類型,T類型限定爲Fruit,因此這裏能夠直接調用Fruit裏面的方法getWeight()(確切的說是能夠調用Fruit裏面能夠被子類繼承的方法,由於限定以後,實參也能夠是Fruit的子類),若是不加限定,那麼T就默認是Object類型,t1.getWeight()就會報錯由於Object裏面沒有這個方法(調用Object裏面的方法是能夠的)。這就是是類型限定的兩個好處。this

類型也可使用接口限定,好比 ,這樣的話,只有實現了MyInterface接口的類才能做爲實際類型參數。下面是類型限定的幾個注意點: .net

  1. 無論限定是類仍是接口,統一都使用extends關鍵字
  2. 可使用&符號給出多個限定,例如:<U extends Number & MyInterface1 & MyInterface2>
  3. 多個限制只能有一個類名,其餘都是接口名,且類名在最前面。

3、通配符

先看三行代碼

Fruit f = new Apple();
Fruit[] farray = new Apple[10];
ArrayList<Fruit> flist = new ArrayList<Apple>();

第一行的寫法是很常見的,父類引用指向子類對象,這是java多態的表現。相似的,第二行父類數組的引用指向子類數組對象在java中也是能夠的,這稱爲數組的協變。Java把數組設計爲協變的,對此是有爭議的,有人認爲這是一種缺陷。

雖然Apple[]能夠「向上轉型」爲Fruit[],但數組元素的實際類型仍是Apple,因此只能向數組中放入Apple或者Apple的子類。在上面的代碼中,向數組中放入了Fruit對象和Orange對象,對於編譯器來講,這是能夠經過編譯的,可是在運行時期,JVM可以知道數組的實際類型是Apple[],因此當其它對象加入數組的時候在運行期會拋出異常。

由上可知,協變的缺陷在於可能的異常發生在運行期,而編譯期間沒法檢查,泛型設計的目的之一就是避免這種問題,因此泛型是不支持協變的,也就是說,上面的第三行代碼是編譯不經過的。可是,有時候是須要創建這種「向上轉型」的關係的,好比定義一個方法,打印出任意類型的List中的全部數據,示例以下:

public void printCollection(List<Object> collection) {
    for (Object obj : collection) {
        System.out.println(obj);
    }
}

------------------------------------
List<Integer> listInteger =new ArrayList<Integer>();
List<String> listString =new ArrayList<String>();

printCollection(listInteger); //編譯錯誤
printCollection(listString); //編譯錯誤

由於泛型不支持協變,即List<Object> collection = new ArrayList<Integer>();沒法經過編譯,因此printCollection(listInteger)就會報錯。
這時就須要使用通配符來解決,通配符<?>,用來表示某種特定的類型,可是不知道這個類型究竟是什麼。例以下面的例子都是合法的:

List<?> collection1 = new ArrayList<Fruit>();
List<?> collection2 = new ArrayList<Number>();
List<?> collection3 = new ArrayList<String>();
List<?> collection4 = new ArrayList<任意類型>();
// 對比不合法的 List<Fruit> flist = new ArrayList<Apple>();

因此printCollection()方法改爲下面這樣便可:

public void printCollection(List<?> collection) {
    for (Object obj : collection) {
        System.out.println(obj);
    }
}

這就是通配符的簡單用法。須要注意的是,由於不知道 "?" 類型究竟是什麼,因此List<?> collection中的collection不能調用帶泛型參數的方法,可是能夠調用與泛型參數類型無關的方法,以下:

collection.add("a"); //錯誤,由於add方法參數是泛型E
collection.size(); //正確,由於無參即與泛型參數類型E無關
collection.contains("a"); //正確,由於contains參數是Object類型,與泛型參數類型E無關

注:collection.add(null);是能夠的,除了null其餘任何類型都不能夠,Object也不行。

通配符的邊界

通配符可使用extends和super關鍵字來限制:

  • List<? extends Number> 表示不肯定參數類型,但必須是Number類型或者Number子類類型,這是上邊界限定
  • List<? super Number> 表示不肯定參數類型,但必須是Number類型或者Number的父類類型,這是下邊界限定
  • List<?> 表示未受限的通配符,至關於 List<? extends Object>

注意區分 泛型變量的類型限定通配符的邊界限定

  1. 泛型變量的類型限定,是在定義泛型類的時候對聲明的泛型參數進行限定(限定的是形式參數)
    public class Box {}
  2. 通配符的邊界限定,是在定義化泛型類的引用的時候對實際泛型參數進行限定(限定的是實際參數)
    List<? extends Number> listInteger =new ArrayList ();

泛型變量的類型限定只能使用extends關鍵字,通配符的邊界限定可使用extends或super來限定上邊界或下邊界。


4、Java泛型的原理-類型擦除

Java中的泛型是經過類型擦除來實現的僞泛型。類型擦除指的是從泛型類型中清除類型參數的相關信息,而且在必要的時候添加類型檢查和類型轉換的方法。類型擦除能夠簡單的理解爲將泛型java代碼轉換爲普通java代碼,只不過編譯器更直接點,將泛型java代碼直接轉換成普通的java字節碼,看下面的例子:

泛型的Java代碼以下:

class Pair<T> {
    private T value;
    public T getValue() {
        return value;
    }
    public void setValue(T  value) {
        this.value = value;
    }
}

泛型Java代碼,通過編譯器編譯後,會擦除泛型信息,將泛型代碼轉換爲以下的普通Java代碼:

class Pair {
    private Object value;
    public Object getValue() {
        return value;
    }
    public void setValue(Object  value) {
        this.value = value;
    }
}

由上面的例子可知,泛型擦除的結果就是用Object替換T,最終生成一個普通的類。上面的例子替換成Obejct是由於在Pair 中,T是一個無限定的類型變量,因此用Object替換。若是T被限定了,好比 ,那麼擦除後就用Number替換泛型類裏面的T。多個限定的話,使用第一個邊界的類型變量來做爲原始類型。
至此能夠知道,類型擦除的過程:

  1. 移除全部的類型參數。
  2. 將全部移除的泛型參數用其限定的最左邊界類型替換。(多個限定的話,其餘限定必定是接口,並且實際參數必定實現了這些接口,不然不合法,編譯不經過,因此用最左邊界類型替換)

泛型只存在於代碼中,泛型信息在編譯時都會被擦除,因此虛擬機中沒有泛型,只有普通類和普通方法。


Java泛型的一些注意問題

使用泛型時會有一些問題和限制,大部分是由類型擦除引發的,因此只要記住:泛型擦除後留下的只有原始類型。那麼大部分問題都是很容易理解的。好比下面的例子:

public void test(List<String> list){

}

public void test(List<Integer> list){

}

兩個方法通過泛型擦除後,都只留下原始類型List,因此它們是同一個方法而不是方法的重載,若是這兩個方法在同一個類中同時存在,編譯器是會報錯的。

其餘更多問題以下:

  1. 先檢查,在編譯,以及檢查編譯的對象和引用傳遞的問題
  2. 自動類型轉換
  3. 類型擦除與多態的衝突和解決方法
  4. 泛型類型變量不能是基本數據類型
  5. 運行時類型查詢
  6. 異常中使用泛型的問題
  7. 數組(這個不屬於類型擦除引發的問題)
  8. 類型擦除後的衝突
  9. 泛型在靜態方法和靜態類中的問題

這些問題的答案在:java泛型(二)、泛型的內部原理:類型擦除以及類型擦除帶來的問題,本文也是學習這篇博客和一些其餘博客後的一個總結。


參考文章:
Java泛型-類型擦除
java泛型(一)、泛型的基本介紹和使用
java泛型(二)、泛型的內部原理:類型擦除以及類型擦除帶來的問題
Java 泛型總結(三):通配符的使用

相關文章
相關標籤/搜索