Java 泛型使用

泛型是Java中一項十分重要的特性,在Java 5版本被引入,在日常的編程過程中,有很多依賴泛型的場景,尤其是在集合容器類的使用過程中,更是離不開泛型的影子。

泛型的作用

泛型提供的功能有:參數(shù)化類型,以及編譯期類型檢查。

1 參數(shù)化類型

在方法的定義中,方法的參數(shù)稱為形參,在實際調(diào)用方法時傳遞實參。泛型的使用中,可以將類型定義為一個參數(shù),在實際使用時再傳遞具體類型。將泛型這種使用方式稱之為參數(shù)化類型。

在集合類的使用中,若不使用泛型,則需要對每一種元素類型設計相同的集合操作,例如:

class ListInteger{
    //...
}
class ListDouble{
    //...
}

通過泛型的使用,可以避免這種重復定義的現(xiàn)象,定義一套集合操作,來應對所有元素類型,例如:

class List<E>{
    //...
}

在使用中傳遞不同的元素類型給List即可。

這里使用的字符E并無特殊含義,只是為了便于理解而已。泛型中通常使用的字符及表示意義為:
K: 鍵值對中的key
V: 鍵值對中的value
E: 集合中的element
T: 類的類型type

2 編譯期類型檢查

對于集合ArrayList而言,若不指定具體元素類型,則使用過程中可能出現(xiàn)以下情況:

List list = new ArrayList();
list.add("abc");
list.add(123);

for (Object obj : list) {
    String e = (String) obj;//ClassCastException
}

這段代碼在編譯期沒問題,運行時會報出java.lang.ClassCastException。

這種對集合的使用方式存在兩個問題:一是add添加元素時,因為元素聲明為Object類型,任意類型元素都可以添加到集合中,所以在添加元素時需要使用者自己注意選擇的元素類型;二是get取元素時需要強制類型轉(zhuǎn)換,需要開發(fā)人員記住操作的元素類型,否則可能拋出ClassCastException異常。

在聲明集合時指定元素類型則可以避免以上兩種問題:

List<String> list = new ArrayList<String>();
list.add("abc");
//list.add(123); compile error

for (String obj : list) {
    String e = obj;
}

通過泛型的使用,指定集合元素的類型,則可以在編譯期就進行元素類型檢查,并且get獲取元素時無需進行強制類型轉(zhuǎn)換。

這里稱獲取元素無需進行強制類型轉(zhuǎn)換,其實并不準確,嚴格來講,使用泛型在進行獲取元素操作時,進行的是隱式類型轉(zhuǎn)換,所以仍然存在強制類型轉(zhuǎn)換的操作。

ArrayList中的隱式類型轉(zhuǎn)換:

public E get(int index) {
    rangeCheck(index);
    return elementData(index);
}

E elementData(int index) {
    return (E) elementData[index];
}

泛型的使用

泛型可以應用于定義泛型類、泛型接口和泛型方法。

1 泛型類

泛型類的定義方式較為簡單,通過將類型抽象為參數(shù),附加在類名稱后,即可完成泛型類的定義,示例:

public class Test {
    public static void main(String[] args) {
        User<Integer> user = new User<>();
        user.setAttribute(123);
//        user.setAttribute("abc");compile error
        Integer attribute = user.getAttribute();
    }
}

class User<T> {
    private T attribute;

    public User() {
    }

    public T getAttribute() {
        return this.attribute;
    }

    public void setAttribute(T attribute) {
        this.attribute = attribute;
    }
}

通過使用泛型類,可以在編譯期進行參數(shù)類型檢查,并且使用時無需進行強制類型轉(zhuǎn)換。

2 泛型接口

泛型接口的使用與泛型類較為相似,在接口名稱后添加表示類型的字符即可,示例:

interface Person<T> {
    T getAttribute();

    void setAttribute(T attribute);
}
3 泛型方法

在前面的泛型類中定義的如下方法:

    public T getAttribute() {
        return this.attribute;
    }

    public void setAttribute(T attribute) {
        this.attribute = attribute;
    }

雖然使用了參數(shù)化類型,但是并不算是泛型方法,因為這些方法中使用的參數(shù)類型是泛型類定義的。泛型方法中定義了自己使用的類型,示例:

public <T> void genericsMethod(T parameter){
    //...
}

泛型與繼承

在泛型的使用中,關于繼承方面需要注意,示例:

public class Test {
    public static void main(String[] args) {
        A<Number> aNumber = new A<>();
        A<Integer> aInteger = new A<>();
//        aNumber = aInteger; compile error
        System.out.println(aNumber.getClass() == aInteger.getClass()); // true
    }
    static class A<T>{}
}

雖然IntegerNumber的子類型,但是A<Integer>并不是A<Number>的子類型。

事實上,編譯器會在編譯階段進行類型檢查后,會擦除泛型的類型信息,也就是說在運行期A<Integer>A<Number>是同一個類。

對于泛型容器類List<E>,在進行泛型擦除后,記錄的元素類型為其聲明的最左邊父類型,此處即為Object類型,示例:

public class Test {
    public static void main(String[] args) throws Exception {
        List<Integer> integers = new ArrayList<>();
        integers.getClass().getDeclaredMethod("add", Object.class).invoke(integers, "abc");
    }
}

代碼在編譯期和運行期都沒問題,在編譯生成的.class文件中,Integer元素類型被擦除后,容器的元素類型記錄為Object類型。


泛型使用中的繼承定義方式如下:

public class Test {
    public static void main(String[] args) {
        A<Integer> a = new A<>();
        B<Integer> b = new B<>();
        a = b;
    }
}
class A<T>{}
class B<T> extends A<T>{}

在繼承關系中使用同一個參數(shù)類型,以此實現(xiàn)泛型類的繼承。在JDKArrayList<E>、List<E>Collection<E>采用的就是這種方式。

但是這種繼承方式依然不能滿足前面提到的使用場景,例如如下使用List方式:

public class Test {
    public static void main(String[] args) {
        List<Number> numberList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
//        numberList = integerList; compile error
    }
}

雖然IntegerNumber的子類型,但List<Integer>卻不是List<Number>的子類型,問題與前面的示例中相同。

通配符

通配符號?是一種實參類型,表示類型不確定的意思,或者表示任意一種類型,選擇?作為類型的目的是為了匹配更大范圍的類型,所以這里?是一種具體的類型。

這里稱?類型不確定,又稱?是一種具體的類型,這種說法是相對于前面的類型參數(shù)T而言的,T表示類型形參,使用時被替代為傳入的具體類型,而?就是一種具體類型,不會被別的具體類型替代。

在前面有關泛型的繼承關系中,遇到List<Integer>不是List<Number>的子類型問題,可以使用通配符號?表示具體類型,這樣則可以匹配任意的參數(shù)類型,示例:

public class Test {
    public static void main(String[] args) {
        List<?> numberList = new ArrayList<>();
        List<Integer> integerList = new ArrayList<>();
        numberList = integerList; 
    }
}

既然?可以表示所有類型,當然也可以表示Integer類型,所以代碼可以編譯通過。

在平常的使用中,類型的選擇范圍并非如此隨意,更多時候在定義泛型類、接口或方法時,限定了能夠使用的類型范圍。

1 限定上界

使用extends關鍵字限定參數(shù)類型能夠選擇的上界,示例:

public class Test {
    public static void main(String[] args) {
        GenericsClass<Integer> integerObj = new GenericsClass<>();
//        GenericsClass<String> stringObj = new GenericsClass<>(); compile error
        
        Test.genericsMethod1(new ArrayList<Integer>());
//        Test.genericsMethod1(new ArrayList<String>()); compile error

        Test.genericsMethod2(new ArrayList<Integer>());
//        Test.genericsMethod2(new ArrayList<String>()); compile error
    }
    static class GenericsClass<T extends Number>{
        //...
    }
    static <T extends Number> void genericsMethod1(List<T> list) {
//        list.add(1); compile error
    }
    static void genericsMethod2(List<? extends Number> list) {
//        list.add(1); compile error
    }
}

GenericsClass類中通過<T extends Number>限定參數(shù)類型為Number的子類型,genericsMethod1、genericsMethod2同樣使用extends關鍵字限定類型上界。

genericsMethod1genericsMethod2分別使用了T?作為參數(shù)類型符號,在限定類型范圍上,兩者作用相同。不同之外在于,使用T表示類型形參,在genericsMethod1方法體內(nèi)可以引用T類型相關的操作,但是?則無法引用。

這里需要注意一點,若使用具有上界的泛型來作為集合的元素類型時,因為此時無法確定集合的元素類型,所以無法向集合中添加元素,示例:

    static <T extends Number> void genericsMethod1(List<T> list) {
//        list.add(1); compile error
    }
    static void genericsMethod2(List<? extends Number> list) {
//        list.add(1); compile error
    }
2 限定下界

使用super關鍵字限定參數(shù)類型能夠選擇的下界,示例:

public class Test {
    public static void main(String[] args) {
        Test.genericsMethod2(new ArrayList<Integer>());
//        Test.genericsMethod2(new ArrayList<String>()); compile error
    }
//    static class GenericsClass<? super Integer>{ compile error
//        //...
//    }
//    static <T super Integer> void genericsMethod1(List<T> list) { compile error
//        //...
//    }
    static void genericsMethod2(List<? super Integer> list) {
        list.add(1); 
    }
}

由示例可知,<? super Integer>的形式限定元素的下界為Integer類型,則此時可以對集合進行添加Integer元素操作。

由示例同樣可知,使用super關鍵字限定參數(shù)類型下界,與使用extends關鍵字限定參數(shù)類型的上界有所不同,最大的區(qū)別就是:類型形參T不能與super關鍵字配合使用。若可以配合使用,則會存在以下問題:

  • <T extends Integer>表示T類為Integer的子類型,則T類型屬性可以訪問Integer類型中的部分屬性;<T super Integer>的描述表示T類為Integer的父類,則T類型屬性不確定其父類為何類,也可能為Serializable,那么此時將不具備任何屬性,因為不確定,所以無法進行操作;

  • <T extends Integer>在編譯時進行類型擦除后,則T屬性將默認為extends繼承的父類中最左邊一個,這里即為Integer;而<T super Integer>描述的類,在進行類型擦除后將無法確定其類型。

根據(jù)以上兩點,在類的描述中,不能使用<T super Integer>的形式限定參數(shù)類型的下界。

通配符的上下界使用有PECS(producer extends, consumer super)原則,producer可以根據(jù)上界進行元素讀取,但是不確定類型,所以無法添加元素;consumer可以根據(jù)下界進行元素添加,但是不確定類型,所以無法讀取元素。

泛型數(shù)組

在普通數(shù)組的使用中,存在如下的情況:

public class Test {
    public static void main(String[] args) {
        Integer[] integers = new Integer[5];
        Object[] objects = integers;
        objects[0] = "abc";
    }
}

這段代碼在編譯期是沒問題的,在運行時會報出ArrayStoreException異常。這種情況稱之為數(shù)組的協(xié)變(covariant),即S類型為T類型的子類型,則S類型數(shù)組為T類型數(shù)組的子類型。

為了避免這種協(xié)變的情況發(fā)生,Java禁止創(chuàng)建具體類型的泛型數(shù)組,否則對于泛型數(shù)組有如下情況,示例來源Java 指導手冊

// Not really allowed.
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Unsound, but passes run time store check
oa[1] = li;

// Run-time error: ClassCastException.
String s = lsa[1].get(0);

如果Java中允許創(chuàng)建具體類型的泛型數(shù)組,則以上代碼在編譯期通過類型檢查,在運行期獲取元素時會報出ClassCastException異常,即無法通過泛型元素的隱式類型轉(zhuǎn)換。

Java雖然禁止創(chuàng)建具體類型的泛型數(shù)組,但并不禁止創(chuàng)建通配符形式的數(shù)組,如下所示,示例來源Java 指導手冊

// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);

雖然發(fā)生運行期錯誤,但是因為通配符的使用,所以在獲取元素時,需要進行顯示類型轉(zhuǎn)換,也就是將元素的類型操作交給開發(fā)人員進行控制。

參考

Type Parameters
Difference between <? super T> and <? extends T> in Java
The Java? Tutorials

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容