Java 泛型原理

泛型是什麼?

考慮如下場景:您但願開發一個用於在應用中傳遞對象的容器。但對象類型並不老是相同。所以,須要開發一個可以存儲各類類型對象的容器。安全

鑑於這種狀況,要實現此目標,顯然最好的辦法是開發一個可以存儲和檢索 Object 類型自己的容器,而後在將該對象用於各類類型時進行類型轉換。bash

實例1中的類演示瞭如何開發此類容器。編輯器

public class ObjectContainer {
    private Object obj;

    /**
     * @return the obj
     */
    public Object getObj() {
        return obj;
    }

    /**
     * @param obj the obj to set
     */
    public void setObj(Object obj) {
        this.obj = obj;
    }
    
}


ObjectContainer myObj = new ObjectContainer();

// store a string
myObj.setObj("Test");
System.out.println("Value of myObj:" + myObj.getObj());
// store an int (which is autoboxed to an Integer object)
myObj.setObj(3);
System.out.println("Value of myObj:" + myObj.getObj());

List objectList = new ArrayList();
objectList.add(myObj);
// We have to cast and must cast the correct type to avoid ClassCastException!
String myStr = (String) ((ObjectContainer)objectList.get(0)).getObj(); 
System.out.println("myStr: " + myStr);
複製代碼

雖然這個容器會達到預期效果,但就咱們的目的而言,它並非最合適的解決方案。它不是類型安全的,而且要求在檢索封裝對象時使用顯式類型轉換,所以有可能引起異常函數

使用泛型能夠開發一個更好的解決方案,在實例化時爲所使用的容器分配一個類型,也稱泛型類型,這樣就能夠建立一個對象來存儲所分配類型的對象。ui

泛型類型是一種類型參數化的類或接口,這意味着能夠經過執行泛型類型調用 分配一個類型,將用分配的具體類型替換泛型類型。而後,所分配的類型將用於限制容器內使用的值,這樣就無需進行類型轉換,還能夠在編譯時提供更強的類型檢查。this

實例2演示瞭如何建立與先前建立的容器相同的容器,但此次使用泛型類型參數,而不是 Object 類型。spa

public class GenericContainer<T> {
    private T obj;

    public GenericContainer(){
    }
    
    // Pass type in as parameter to constructor
    public GenericContainer(T t){
        obj = t;
    }

    /**
     * @return the obj
     */
    public T getObj() {
        return obj;
    }

    /**
     * @param obj the obj to set
     */
    public void setObj(T t) {
        obj = t;
    }
}


//要使用泛型容器,必須在實例化時使用尖括號表示法指定容器類型。
//所以,如下代碼將實例化一個 Integer 類型的GenericContainer,並將其分配給 myInt 字段。
GenericContainer<Integer> myInt =  new GenericContainer<>();
//或者
GenericContainer<Integer> myInt =  new GenericContainer<Integer>();


//若是咱們嘗試在已經實例化的容器中存儲其餘類型的對象,代碼將沒法編譯
myInt.setObj(3);  // OK
myInt.setObj("Int"); // Won't Compile 複製代碼

最顯著的差別是類定義包含 ,類字段 obj 再也不是 Object 類型,而是泛型類型 T。類定義中的尖括號之間是類型參數部分,介紹類中將要使用的類型參數(或多個參數)。T 是與此類中定義的泛型類型關聯的參數。code

使用泛型的好處

一個最重要的好處是更強的類型檢查,由於避開運行時可能引起的 ClassCastException 能夠節省時間。對象

另外一個好處是消除了類型轉換,這意味着能夠用更少的代碼,由於編譯器確切知道集合中存儲的是何種類型。繼承

如何使用泛型

泛型有許多不一樣用例。本文在前面的示例中介紹了生成泛型對象類型的用例。這對於在類和接口層面瞭解泛型語法是個很好的起點。

類簽名包含一個類型參數部分,包括在類名後的尖括號 (< >) 內

例如:

public class GenericContainer<T> {
...
複製代碼

類型參數(又稱類型變量)用做佔位符,指示在運行時爲類分配類型。根據須要,可能有一個或多個類型參數,而且能夠用於整個類。根據慣例,類型參數是單個大寫字母,該字母用於指示所定義的參數類型。下面列出每一個用例的標準類型參數:

  • E:元素
  • K:鍵
  • N:數字
  • T:類型
  • V:值
  • S、U、V 等:多參數狀況中的第 二、三、4 個類型

在上面的示例中,T 指示將分配的類型,所以可在實例化時爲 GenericContainer 分配任何有效類型。注意,T 參數用於整個類,指示實例化時指定的類型。使用下面這行代碼實例化對象時,將用 String 類型替換全部 T 參數:

GenericContainer<String> stringContainer = new GenericContainer<String>();
複製代碼

泛型也可用於構造函數中,傳遞類域初始化所需的類型參數。GenericContainer 的構造函數容許在實例化時傳遞任意類型:

GenericContainer gc1 = new GenericContainer(3);
GenericContainer gc2 = new GenericContainer("Hello");
複製代碼

注意,未分配類型的泛型稱爲原始類型。例如,要建立原始類型的 GenericContainer,可使用如下代碼:

GenericContainer rawContainer = new GenericContainer();
複製代碼

原始類型有時對於實現向後兼容頗有用,但並不適用於平常代碼。原始類型在編譯時無需執行類型檢查,致使代碼在運行時易於出錯。

多種泛型類型

有時,可以在類或接口中使用多種泛型類型頗有幫助。經過在尖括號之間放置一個逗號分隔的類型列表,可在類或接口中使用多個類型參數。

下面實例中的類使用一個接受如下兩種類型的類演示了此概念:T 和 S。

public class MultiGenericContainer<T, S> {
    private T firstPosition;
    private S secondPosition;
   
    public MultiGenericContainer(T firstPosition, S secondPosition){
        this.firstPosition = firstPosition;
        this.secondPosition = secondPosition;
    }
    
    public T getFirstPosition(){
        return firstPosition;
    }
    
    public void setFirstPosition(T firstPosition){
        this.firstPosition = firstPosition;
    }
    
    public S getSecondPosition(){
        return secondPosition;
    }
    
    public void setSecondPosition(S secondPosition){
        this.secondPosition = secondPosition;
    }
    
}

複製代碼

MultiGenericContainer 類可用於存儲兩個不一樣對象,每一個對象的類型可在實例化時指定。

容器的用法以下

MultiGenericContainer<String, String> mondayWeather =
        new MultiGenericContainer<String, String>("Monday", "Sunny");
MultiGenericContainer<Integer, Double> dayOfWeekDegrees = 
        new MultiGenericContainer<Integer, Double>(1, 78.0);

String mondayForecast = mondayWeather.getFirstPosition();
// The Double type is unboxed--to double, in this case. More on this in next section!
double sundayDegrees = dayOfWeekDegrees.getSecondPosition();
複製代碼

有界類型

咱們常常會遇到這種狀況,須要指定泛型類型,但又但願能夠控制指定的類型,而非不加限制。有界類型 在類型參數部分指定 extendssuper 關鍵字,分別用上限或下限限制類型,從而限制泛型類型的邊界。

若是但願將某類型限制爲特定類型或特定類型的子類型,請使用如下表示法:

<T extends UpperBoundType>
複製代碼

一樣,若是但願將某個類型限制爲特定類型或特定類型的超類型,請使用如下表示法:

<T super LowerBoundType>
複製代碼

什麼是PECS?

PECS指「Producer Extends,Consumer Super」。 若是你是想遍歷collection,並對每一項元素操做時,此時這個集合是生產者(生產元素),應該使用 Collection<? extends Thing>。 若是你是想添加元素到collection中去,那麼此時集合是消費者(消費元素)應該使用Collection<? super Thing>。

泛型方法

有時,咱們可能不知道傳入方法的參數類型。在方法級別應用泛型能夠解決此類問題。方法參數能夠包含泛型類型,方法也能夠包含泛型返回類型。

假設咱們要開發一個接受 Number 類型的計算器類。泛型可用於確保可將任何 Number 類型做爲參數傳遞給此類的計算方法。

例如,以下示例中的 add() 方法演示瞭如何使用泛型限制兩個參數的類型,確保其包含 Number 的上限:

public static <N extends Number> double add(N a, N b){
    double sum = 0;
    sum = a.doubleValue() + b.doubleValue();
    return sum;
}  
複製代碼

經過將類型限制爲 Number,您能夠將 Number 子類的任何對象做爲參數傳遞。此外,經過將類型限制爲 Number,咱們還能夠確保傳遞給該方法的任何參數將包含 doubleValue() 方法。要查看實際效果,若是您想添加一個 Integer 和一個 Float,能夠按以下所示調用該方法:

double genericValue1 = Calculator.add(3, 3f);
複製代碼

通配符

某些狀況下,編寫指定未知類型的代碼頗有用。問號 ? 通配符可用於使用泛型代碼表示未知類型。通配符可用於參數、字段、局部變量和返回類型。但最好不要在返回類型中使用通配符,由於確切知道方法返回的類型更安全。

假設咱們想編寫一個方法來驗證指定的 List 中是否存在指定的對象。咱們但願該方法接受兩個參數:一個是未知類型的 List,另外一個是任意類型的對象。

public static <T> void checkList(List<?> myList, T obj){
        if(myList.contains(obj)){
            System.out.println("The list contains the element: " + obj);
        } else {
            System.out.println("The list does not contain the element: " + obj);
        }
    }
複製代碼

使用示例

// Create List of type Integer
List<Integer> intList = new ArrayList<Integer>();
intList.add(2);
intList.add(4);
intList.add(6);

// Create List of type String
List<String> strList = new ArrayList<String>();
strList.add("two");
strList.add("four");
strList.add("six");

// Create List of type Object
List<Object> objList = new ArrayList<Object>();
objList.add("two");
objList.add("four");
objList.add(strList);

checkList(intList, 3); 
// Output:  The list [2, 4, 6] does not contain the element: 3

checkList(objList, strList); 
/* Output:  The list [two, four, [two, four, six]] contains 
the element: [two, four, six] */

checkList(strList, objList);
/* Output:  The list [two, four, six] does not contain 
the element: [two, four, [two, four, six]] */
複製代碼

有時要使用上限或下限限制通配符。與指定帶邊界的泛型類型極其類似,指定 extends 或 super 關鍵字加上通配符,後面跟用於上限或下限的類型,便可聲明帶邊界的通配符類型。

例如,若是咱們要更改 checkList 方法使其只接受擴展 Number 類型的 List,可按清單 14 所示編寫代碼。

public static <T> void checkNumber(List<? extends Number> myList, T obj){
    if(myList.contains(obj)){
        System.out.println("The list " + myList + " contains the element: " + obj);
    } else {
        System.out.println("The list " + myList + " does not contain the element: " + obj);
    }
}
複製代碼

總結

泛型其實說白了就是應用在編譯時期是給編譯器使用的技術,到了運行時期,泛型就不存在啦。這是由於,編輯器檢查了泛型的類型正確以後,再生成的類文件中是沒有泛型的。

泛型使用注意事項

  • 對象實例化時不指定泛型的話,默認爲:Object。
  • 泛型的指定中不能使用基本數據類型,可使用包裝類替換
  • 靜態方法中不能使用類的泛型
  • 能夠同時綁定多個綁定,用&鏈接
  • 泛型類可能多個參數,此時應將多個參數一塊兒放在尖括號內。好比<E1,E2,E3>
  • 從泛型類派生子類,泛型類型需具體化:繼承泛型類後,子類類型對應類型須要具體化
  • 若是泛型類是一個接口或抽象類,則不可建立泛型類的對象。
相關文章
相關標籤/搜索