Java 泛型原理

zeroXuan發表於2019-04-01

泛型是什麼?

考慮以下場景:您希望開發一個用於在應用中傳遞物件的容器。但物件型別並不總是相同。因此,需要開發一個能夠儲存各種型別物件的容器。

鑑於這種情況,要實現此目標,顯然最好的辦法是開發一個能夠儲存和檢索 Object 型別本身的容器,然後在將該物件用於各種型別時進行型別轉換。

例項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);
複製程式碼

雖然這個容器會達到預期效果,但就我們的目的而言,它並不是最合適的解決方案。它不是型別安全的,並且要求在檢索封裝物件時使用顯式型別轉換,因此有可能引發異常

使用泛型可以開發一個更好的解決方案,在例項化時為所使用的容器分配一個型別,也稱泛型型別,這樣就可以建立一個物件來儲存所分配型別的物件。

泛型型別是一種型別引數化的類或介面,這意味著可以通過執行泛型型別呼叫 分配一個型別,將用分配的具體型別替換泛型型別。然後,所分配的型別將用於限制容器內使用的值,這樣就無需進行型別轉換,還可以在編譯時提供更強的型別檢查。

例項2演示瞭如何建立與先前建立的容器相同的容器,但這次使用泛型型別引數,而不是 Object 型別。

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 是與此類中定義的泛型型別關聯的引數。

使用泛型的好處

一個最重要的好處是更強的型別檢查,因為避開執行時可能引發的 ClassCastException 可以節省時間。

另一個好處是消除了型別轉換,這意味著可以用更少的程式碼,因為編譯器確切知道集合中儲存的是何種型別。

如何使用泛型

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

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

例如:

public class GenericContainer<T> {
...
複製程式碼

型別引數(又稱型別變數)用作佔位符,指示在執行時為類分配型別。根據需要,可能有一個或多個型別引數,並且可以用於整個類。根據慣例,型別引數是單個大寫字母,該字母用於指示所定義的引數型別。下面列出每個用例的標準型別引數:

  • E:元素
  • K:鍵
  • N:數字
  • T:型別
  • V:值
  • S、U、V 等:多引數情況中的第 2、3、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>
  • 從泛型類派生子類,泛型型別需具體化:繼承泛型類後,子類型別對應型別需要具體化
  • 如果泛型類是一個介面或抽象類,則不可建立泛型類的物件。

相關文章