Java 泛型使用

泛型是Java中一項(xiàng)十分重要的特性薄风,在Java 5版本被引入昔搂,在日常的編程過(guò)程中句惯,有很多依賴(lài)泛型的場(chǎng)景土辩,尤其是在集合容器類(lèi)的使用過(guò)程中支救,更是離不開(kāi)泛型的影子抢野。

泛型的作用

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

1 參數(shù)化類(lèi)型

在方法的定義中指孤,方法的參數(shù)稱(chēng)為形參,在實(shí)際調(diào)用方法時(shí)傳遞實(shí)參。泛型的使用中恃轩,可以將類(lèi)型定義為一個(gè)參數(shù)结洼,在實(shí)際使用時(shí)再傳遞具體類(lèi)型。將泛型這種使用方式稱(chēng)之為參數(shù)化類(lèi)型叉跛。

在集合類(lèi)的使用中松忍,若不使用泛型,則需要對(duì)每一種元素類(lèi)型設(shè)計(jì)相同的集合操作筷厘,例如:

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

通過(guò)泛型的使用鸣峭,可以避免這種重復(fù)定義的現(xiàn)象,定義一套集合操作酥艳,來(lái)應(yīng)對(duì)所有元素類(lèi)型摊溶,例如:

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

在使用中傳遞不同的元素類(lèi)型給List即可。

這里使用的字符E并無(wú)特殊含義充石,只是為了便于理解而已莫换。泛型中通常使用的字符及表示意義為:
K: 鍵值對(duì)中的key
V: 鍵值對(duì)中的value
E: 集合中的element
T: 類(lèi)的類(lèi)型type

2 編譯期類(lèi)型檢查

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

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

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

這段代碼在編譯期沒(méi)問(wèn)題拉岁,運(yùn)行時(shí)會(huì)報(bào)出java.lang.ClassCastException

這種對(duì)集合的使用方式存在兩個(gè)問(wèn)題:一是add添加元素時(shí)惰爬,因?yàn)樵芈暶鳛?code>Object類(lèi)型膛薛,任意類(lèi)型元素都可以添加到集合中,所以在添加元素時(shí)需要使用者自己注意選擇的元素類(lèi)型补鼻;二是get取元素時(shí)需要強(qiáng)制類(lèi)型轉(zhuǎn)換哄啄,需要開(kāi)發(fā)人員記住操作的元素類(lèi)型,否則可能拋出ClassCastException異常风范。

在聲明集合時(shí)指定元素類(lèi)型則可以避免以上兩種問(wèn)題:

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

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

通過(guò)泛型的使用咨跌,指定集合元素的類(lèi)型,則可以在編譯期就進(jìn)行元素類(lèi)型檢查硼婿,并且get獲取元素時(shí)無(wú)需進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換锌半。

這里稱(chēng)獲取元素?zé)o需進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換,其實(shí)并不準(zhǔn)確寇漫,嚴(yán)格來(lái)講刊殉,使用泛型在進(jìn)行獲取元素操作時(shí),進(jìn)行的是隱式類(lèi)型轉(zhuǎn)換州胳,所以仍然存在強(qiáng)制類(lèi)型轉(zhuǎn)換的操作记焊。

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

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

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

泛型的使用

泛型可以應(yīng)用于定義泛型類(lèi)、泛型接口和泛型方法栓撞。

1 泛型類(lèi)

泛型類(lèi)的定義方式較為簡(jiǎn)單遍膜,通過(guò)將類(lèi)型抽象為參數(shù)碗硬,附加在類(lèi)名稱(chēng)后,即可完成泛型類(lèi)的定義瓢颅,示例:

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;
    }
}

通過(guò)使用泛型類(lèi)恩尾,可以在編譯期進(jìn)行參數(shù)類(lèi)型檢查,并且使用時(shí)無(wú)需進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換挽懦。

2 泛型接口

泛型接口的使用與泛型類(lèi)較為相似翰意,在接口名稱(chēng)后添加表示類(lèi)型的字符即可,示例:

interface Person<T> {
    T getAttribute();

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

在前面的泛型類(lèi)中定義的如下方法:

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

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

雖然使用了參數(shù)化類(lèi)型信柿,但是并不算是泛型方法猎物,因?yàn)檫@些方法中使用的參數(shù)類(lèi)型是泛型類(lèi)定義的。泛型方法中定義了自己使用的類(lèi)型角塑,示例:

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

泛型與繼承

在泛型的使用中蔫磨,關(guān)于繼承方面需要注意,示例:

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的子類(lèi)型圃伶,但是A<Integer>并不是A<Number>的子類(lèi)型堤如。

事實(shí)上,編譯器會(huì)在編譯階段進(jìn)行類(lèi)型檢查后窒朋,會(huì)擦除泛型的類(lèi)型信息搀罢,也就是說(shuō)在運(yùn)行期A<Integer>A<Number>是同一個(gè)類(lèi)。

對(duì)于泛型容器類(lèi)List<E>侥猩,在進(jìn)行泛型擦除后榔至,記錄的元素類(lèi)型為其聲明的最左邊父類(lèi)型,此處即為Object類(lèi)型欺劳,示例:

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");
    }
}

代碼在編譯期和運(yùn)行期都沒(méi)問(wèn)題唧取,在編譯生成的.class文件中,Integer元素類(lèi)型被擦除后划提,容器的元素類(lèi)型記錄為Object類(lèi)型枫弟。


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

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>{}

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

但是這種繼承方式依然不能滿(mǎn)足前面提到的使用場(chǎng)景伊履,例如如下使用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的子類(lèi)型韩容,但List<Integer>卻不是List<Number>的子類(lèi)型,問(wèn)題與前面的示例中相同唐瀑。

通配符

通配符號(hào)?是一種實(shí)參類(lèi)型群凶,表示類(lèi)型不確定的意思,或者表示任意一種類(lèi)型介褥,選擇?作為類(lèi)型的目的是為了匹配更大范圍的類(lèi)型座掘,所以這里?是一種具體的類(lèi)型。

這里稱(chēng)?類(lèi)型不確定柔滔,又稱(chēng)?是一種具體的類(lèi)型溢陪,這種說(shuō)法是相對(duì)于前面的類(lèi)型參數(shù)T而言的,T表示類(lèi)型形參睛廊,使用時(shí)被替代為傳入的具體類(lèi)型形真,而?就是一種具體類(lèi)型,不會(huì)被別的具體類(lèi)型替代超全。

在前面有關(guān)泛型的繼承關(guān)系中咆霜,遇到List<Integer>不是List<Number>的子類(lèi)型問(wèn)題,可以使用通配符號(hào)?表示具體類(lèi)型嘶朱,這樣則可以匹配任意的參數(shù)類(lèi)型蛾坯,示例:

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

既然?可以表示所有類(lèi)型,當(dāng)然也可以表示Integer類(lèi)型疏遏,所以代碼可以編譯通過(guò)脉课。

在平常的使用中,類(lèi)型的選擇范圍并非如此隨意财异,更多時(shí)候在定義泛型類(lèi)倘零、接口或方法時(shí),限定了能夠使用的類(lèi)型范圍戳寸。

1 限定上界

使用extends關(guān)鍵字限定參數(shù)類(lèi)型能夠選擇的上界呈驶,示例:

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類(lèi)中通過(guò)<T extends Number>限定參數(shù)類(lèi)型為Number的子類(lèi)型,genericsMethod1疫鹊、genericsMethod2同樣使用extends關(guān)鍵字限定類(lèi)型上界袖瞻。

genericsMethod1genericsMethod2分別使用了T?作為參數(shù)類(lèi)型符號(hào),在限定類(lèi)型范圍上拆吆,兩者作用相同虏辫。不同之外在于,使用T表示類(lèi)型形參锈拨,在genericsMethod1方法體內(nèi)可以引用T類(lèi)型相關(guān)的操作砌庄,但是?則無(wú)法引用。

這里需要注意一點(diǎn)奕枢,若使用具有上界的泛型來(lái)作為集合的元素類(lèi)型時(shí)娄昆,因?yàn)榇藭r(shí)無(wú)法確定集合的元素類(lèi)型,所以無(wú)法向集合中添加元素缝彬,示例:

    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關(guān)鍵字限定參數(shù)類(lèi)型能夠選擇的下界萌焰,示例:

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類(lèi)型谷浅,則此時(shí)可以對(duì)集合進(jìn)行添加Integer元素操作扒俯。

由示例同樣可知奶卓,使用super關(guān)鍵字限定參數(shù)類(lèi)型下界,與使用extends關(guān)鍵字限定參數(shù)類(lèi)型的上界有所不同撼玄,最大的區(qū)別就是:類(lèi)型形參T不能與super關(guān)鍵字配合使用夺姑。若可以配合使用,則會(huì)存在以下問(wèn)題:

  • <T extends Integer>表示T類(lèi)為Integer的子類(lèi)型掌猛,則T類(lèi)型屬性可以訪(fǎng)問(wèn)Integer類(lèi)型中的部分屬性盏浙;<T super Integer>的描述表示T類(lèi)為Integer的父類(lèi),則T類(lèi)型屬性不確定其父類(lèi)為何類(lèi)荔茬,也可能為Serializable废膘,那么此時(shí)將不具備任何屬性,因?yàn)椴淮_定慕蔚,所以無(wú)法進(jìn)行操作丐黄;

  • <T extends Integer>在編譯時(shí)進(jìn)行類(lèi)型擦除后,則T屬性將默認(rèn)為extends繼承的父類(lèi)中最左邊一個(gè)孔飒,這里即為Integer孵稽;而<T super Integer>描述的類(lèi),在進(jìn)行類(lèi)型擦除后將無(wú)法確定其類(lèi)型十偶。

根據(jù)以上兩點(diǎn)菩鲜,在類(lèi)的描述中,不能使用<T super Integer>的形式限定參數(shù)類(lèi)型的下界惦积。

通配符的上下界使用有PECS(producer extends, consumer super)原則接校,producer可以根據(jù)上界進(jìn)行元素讀取,但是不確定類(lèi)型狮崩,所以無(wú)法添加元素蛛勉;consumer可以根據(jù)下界進(jìn)行元素添加,但是不確定類(lèi)型睦柴,所以無(wú)法讀取元素诽凌。

泛型數(shù)組

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

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

這段代碼在編譯期是沒(méi)問(wèn)題的坦敌,在運(yùn)行時(shí)會(huì)報(bào)出ArrayStoreException異常侣诵。這種情況稱(chēng)之為數(shù)組的協(xié)變(covariant),即S類(lèi)型為T類(lèi)型的子類(lèi)型狱窘,則S類(lèi)型數(shù)組為T類(lèi)型數(shù)組的子類(lèi)型杜顺。

為了避免這種協(xié)變的情況發(fā)生,Java禁止創(chuàng)建具體類(lèi)型的泛型數(shù)組蘸炸,否則對(duì)于泛型數(shù)組有如下情況躬络,示例來(lái)源Java 指導(dǎo)手冊(cè)

// 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)建具體類(lèi)型的泛型數(shù)組,則以上代碼在編譯期通過(guò)類(lèi)型檢查搭儒,在運(yùn)行期獲取元素時(shí)會(huì)報(bào)出ClassCastException異常穷当,即無(wú)法通過(guò)泛型元素的隱式類(lèi)型轉(zhuǎn)換提茁。

Java雖然禁止創(chuàng)建具體類(lèi)型的泛型數(shù)組,但并不禁止創(chuàng)建通配符形式的數(shù)組馁菜,如下所示茴扁,示例來(lái)源Java 指導(dǎo)手冊(cè)

// 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ā)生運(yùn)行期錯(cuò)誤,但是因?yàn)橥ㄅ浞氖褂没鸬耍栽讷@取元素時(shí)丹弱,需要進(jìn)行顯示類(lèi)型轉(zhuǎn)換德撬,也就是將元素的類(lèi)型操作交給開(kāi)發(fā)人員進(jìn)行控制铲咨。

參考

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蜓洪,隨后出現(xiàn)的幾起案子纤勒,更是在濱河造成了極大的恐慌,老刑警劉巖隆檀,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件摇天,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡恐仑,警方通過(guò)查閱死者的電腦和手機(jī)泉坐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)裳仆,“玉大人腕让,你說(shuō)我怎么就攤上這事∑缯澹” “怎么了纯丸?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)静袖。 經(jīng)常有香客問(wèn)我觉鼻,道長(zhǎng),這世上最難降的妖魔是什么队橙? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任坠陈,我火速辦了婚禮,結(jié)果婚禮上捐康,老公的妹妹穿的比我還像新娘畅姊。我一直安慰自己,他們只是感情好吹由,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布若未。 她就那樣靜靜地躺著,像睡著了一般倾鲫。 火紅的嫁衣襯著肌膚如雪粗合。 梳的紋絲不亂的頭發(fā)上萍嬉,一...
    開(kāi)封第一講書(shū)人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音隙疚,去河邊找鬼壤追。 笑死,一個(gè)胖子當(dāng)著我的面吹牛供屉,可吹牛的內(nèi)容都是我干的行冰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼伶丐,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼悼做!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哗魂,我...
    開(kāi)封第一講書(shū)人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤肛走,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后录别,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體朽色,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年组题,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了葫男。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡崔列,死狀恐怖梢褐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情峻呕,我是刑警寧澤利职,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站瘦癌,受9級(jí)特大地震影響猪贪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜讯私,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一热押、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧斤寇,春花似錦桶癣、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春间雀,著一層夾襖步出監(jiān)牢的瞬間悔详,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工惹挟, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茄螃,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓连锯,卻偏偏與公主長(zhǎng)得像归苍,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子运怖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容