JAVA泛型教程

泛型

什么是泛型

在強(qiáng)類型語言中,可以先不設(shè)置參數(shù)類型,用某個(gè)符號(hào)作為占位符.最后在運(yùn)行時(shí)指定參數(shù)類型來替換.

為什么要使用泛型

  • 動(dòng)態(tài)化參數(shù),代碼編寫可以更加靈活、復(fù)用性高
  • 類型安全,避免手動(dòng)的類型轉(zhuǎn)換.保證在運(yùn)行時(shí)出現(xiàn)的錯(cuò)誤能提早放到編譯時(shí)檢查
  • 解耦類或方法所使用的類型之間的約束

java的泛型

要求

java的泛型是從jdk1.5之后引入的.所以使用泛型
的最低要求是jdk1.5

為什么會(huì)在jdk1.5之后引入泛型

最主要的原因就是為了重寫容器相關(guān)的類(Collection).如果沒有泛型,都使用Object來代替,那么就會(huì)出現(xiàn)大量的類型轉(zhuǎn)換與模板代碼,復(fù)用性低.

使用場(chǎng)景

  • 暫時(shí)不指定類型,而是稍后再?zèng)Q定具體使用什么類型
  • 限制其類型,使類型需要保持一直
  • 大量的樣板重復(fù)代碼,只是類型不同

泛型語法

在類上編寫泛型

用<>表示泛型,T表示占位符類型,可以自定義不一定叫T.這樣就可以先不指定類型,最后在調(diào)用時(shí)指定.

  • 示例

public class Holder<T> {

    // 先不指定類型
    private T value;

    public void set(T val) {
        value = val;
    }

    public T get() {
        return value;
    }

    public static void main(String[] args) {
        // 在調(diào)用時(shí)指定類型
        Holder<String> holder = new Holder<>();
        holder.set("test");
        System.out.println(holder.get());
    }

}

在方法上編寫泛型

摘自Thinking in java泛型設(shè)計(jì)的基本原則,如果泛型方法能夠代替泛型類,應(yīng)該盡量?jī)?yōu)先使用泛型方法.因?yàn)樵陬惿隙x泛型是全局的,在方法上定義作用在方法上,作用范圍更小.這樣就為類上預(yù)留了泛型參數(shù),適合以后擴(kuò)展.

  • 示例

與類上定義泛型的語法不同的是,只要在返回值之前定義<T>,就能在方法上定義泛型

public class HolderUtils {

    public static <T> T getHolder(Holder<T> holder) {
        return holder.get();
    }

    public static void main(String[] args) {
        Holder<String> holder = new Holder<>();
        holder.set("test");
        System.out.println(HolderUtils.getHolder(holder));
    }
}

泛型的工作原理

從上面的例子中看出,感覺java的泛型就是在運(yùn)行時(shí)指定類型就像 Holder<String> holder = new Holder<>();把指定的類型String 動(dòng)態(tài)的替換成占位符T.實(shí)際本非如此.在java中泛型參數(shù)類型都會(huì)轉(zhuǎn)成Object,擦除實(shí)際的類型.

  • 下面是剛才例子的反編譯的結(jié)果
javap -c Holder.class
Compiled from "Holder.java"
public class generics.Holder<T> {
  public generics.Holder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void set(T);
    Code:
       0: aload_0
       1: aload_1
       // 傳入的T轉(zhuǎn)換成Object,類型被擦除
       2: putfield      #2                  // Field value:Ljava/lang/Object;
       5: return

  public T get();
    Code:
       0: aload_0
        // 返回的T也為Object,類型被擦除
       1: getfield      #2                  // Field value:Ljava/lang/Object;
       4: areturn

  public static void main(java.lang.String[]);
    Code:
       0: new           #3                  // class generics/Holder
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String test
      11: invokevirtual #6                  // Method set:(Ljava/lang/Object;)V
      14: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_1
      18: invokevirtual #8                  // Method get:()Ljava/lang/Object;
      // 需要時(shí)類型檢查,確保類型安全,然后強(qiáng)制類型轉(zhuǎn)換
      21: checkcast     #9                  // class java/lang/String
      24: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return
}

從上面的結(jié)果可以看出泛型的工作原理為

  • 編譯期檢查類型
  • 編譯之后把實(shí)際類型替換成Object類型,參數(shù)實(shí)際類型
  • 在需要時(shí),編譯器自動(dòng)先做類型檢查然后強(qiáng)制轉(zhuǎn)成需要的類型

java 泛型的局限性

java的泛型不是純粹的.在使用泛型時(shí),因?yàn)椴脸裏o法獲取具體的類型.不能使用泛型來執(zhí)行類型相關(guān)的例如new instanceof 反射等操作.

// 因?yàn)楂@取不到真實(shí)的類型,轉(zhuǎn)成Object類型了,則會(huì)出現(xiàn)以下幾個(gè)問題
// 1. 泛型是不能new對(duì)象的
T t  = new T(); // error

// 2. 泛型不能用instanceof
t instanceof // error

public static void main(String[] args) {

    Holder<String> holder = new Holder<>();
    holder.set("test");
    
    // 3.使用getClass().getTypeParameters() 只能獲取占位符 [T], 而不是實(shí)際的類型   
    System.out.println(Arrays.toString(
            holder.getClass().getTypeParameters()));
}

什么是擦除

java的泛型是通過擦除來實(shí)現(xiàn)的.在使用泛型時(shí),任何類型信息都會(huì)被擦除.在泛型代碼內(nèi)部,無法獲得有關(guān)泛型參數(shù)類型的信息.只能獲取到定義泛型的占位標(biāo)識(shí)符

使用擦除的原因

  • java的泛型不是從jdk1.0就已經(jīng)存在的,泛型是從1.5開始的,需要向下兼容

  • 老類庫需要升級(jí)泛型的兼容性.例如有x、y類庫, x 依賴于 y.
    這時(shí)y升級(jí)使用了泛型,x沒有使用泛型.那么勢(shì)必不能對(duì)調(diào)用y的x產(chǎn)生影響.所以x應(yīng)該不具備感知y使用泛型的能力.所以當(dāng)依賴的類庫使用了泛型,則不能對(duì)現(xiàn)有類庫造成影響.所以泛型不是強(qiáng)制的,則類型信息必須被擦除.

如何解決擦除的問題

1. 定義泛型邊界

因?yàn)榉盒偷牟脸?我們獲取不到實(shí)際的類型,可以通過泛型邊界來獲取實(shí)際的類型.如果定義邊界,泛型將會(huì)擦除到第一個(gè)邊界

語法
使用extends關(guān)鍵字,來定義邊界,表示傳入的類型必須是其邊界的類型或者子類

例子:

public class HolderUtils {

    public static <T extends Person> T getHolder(Holder<T> holder) {
        T t = holder.get();
        t.run();
        return t;
    }

    public static void main(String[] args) {
        Holder<String> holder = new Holder<>();
        holder.set("test");
        // System.out.println(HolderUtils.getHolder(holder)); 
       // error 類型不匹配,已經(jīng)限定了只能是Person以及Person的子類

        Holder<Person> holderPerson = new Holder<>();
        holderPerson.set(new Person());
        System.out.println(HolderUtils.getHolder(holderPerson));
    }
}

class Person {

    public void run() {
        System.out.println("run ...");
    }
}


因?yàn)槎x了邊界所以擦除到第一個(gè)邊界類型Person.
這樣就不會(huì)使用Object來代替,就能夠使用邊界類型的方法與屬性.
解決了一部分的擦除問題

2. 傳入類型標(biāo)識(shí)

通過傳入類型標(biāo)識(shí)Class<T> tClass來確保泛型類型,
使用 class.newInstance()來創(chuàng)建對(duì)象,t就能確定其類型信息,
能夠執(zhí)行類型相關(guān)的操作例如instanceof等.這樣就徹底解決了擦除的帶來的類型問題.

public class HolderUtils {

    public static <T extends Person> T getHolder(Holder<T> holder) {
        T t = holder.get();
        t.run();
        return t;
    }

    public static <T> T newHolder(Class<T> tClass) throws IllegalAccessException, InstantiationException {
        T t = tClass.newInstance();
        if (t instanceof Person) {
            System.out.println("type is Person...");
        }
        return t;
    }

    public static void main(String[] args) throws Exception{
        Person person = newHolder(Person.class);
    }
}

class Person {

    public void run() {
        System.out.println("run ...");
    }
}


控制臺(tái)輸出:type is Person...

通配符

在講通配符之前,先要弄清2個(gè)知識(shí)點(diǎn)

  • 什么是泛型容器類型
  • 什么是泛型持有類型

泛型容器類型是可以自動(dòng)向上轉(zhuǎn)型的

// 泛型容器類型 
List list = new ArrayList();
ArrayList -> List OK
表示ArrayList是List的某一種類型

泛型持有類型是不支持向上轉(zhuǎn)型的

class Fruit {}
class Apple extends Fruit {}
// 泛型持有類型
List<Fruit> fruit = new ArrayList<Apple>();
ArrayList -> List error 編譯不通過

雖然Apple能夠自動(dòng)向上轉(zhuǎn)型為Fruit,但是裝有蘋果的籃子不代表就能裝水果.當(dāng)泛型容器類型定義持有對(duì)象類型時(shí),容器類型就不能向上轉(zhuǎn)型.

為什么容器類型定義持有對(duì)象時(shí)就不能自動(dòng)向上轉(zhuǎn)型了

主要原因是會(huì)出現(xiàn)類型轉(zhuǎn)換錯(cuò)誤的問題,泛型的目的就是為了讓運(yùn)行期出現(xiàn)的問題,放到了編譯器處理.增加了代碼的健壯性,避免了類型轉(zhuǎn)換錯(cuò)誤.

class Fruit {}
class Apple extends Fruit {}
class Orange extends Fruit {}
// 數(shù)組可以編譯通過,但是在運(yùn)行期出現(xiàn)異常
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple(); // OK
try {
  fruit[0] = new Orange();// ArrayStoreException
} catch(Exception e) { System.out.println(e); }

控制臺(tái)輸出: java.lang.ArrayStoreException: generics.Orange

// 而泛型直接編譯不通過,避免了運(yùn)行期的類型轉(zhuǎn)換錯(cuò)誤

List<Fruit> fruit = new ArrayList<Apple>(); // 編譯不通過

以上例子就說明了,當(dāng)裝有水果的籃子引用了只能裝蘋果籃子的實(shí)例,而去裝橘子時(shí),就會(huì)出現(xiàn)類型轉(zhuǎn)換錯(cuò)誤.而泛型的好處就是從運(yùn)行期的錯(cuò)誤提前到了編譯器.
那有什么辦法可以讓泛型容器在定義了持有對(duì)象時(shí)即能夠向上轉(zhuǎn)型又能夠保證類型安全呢叽掘?這時(shí)候就可以定義通配符

什么是通配符

通配符就是定義泛型容器的上下界,使其泛型容器類型可以做到類型安全的自動(dòng)類型轉(zhuǎn)換

通配符的語法

使用 <?> 來表示類型范圍

上界

使用 <? extend Fruit> 來表示上界.表示該集合都繼承于Fruit,都能返回Fruit的集合.所以讀取該集合的元素是類型安全的.添加元素是類型不安全的.

List<? extends Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // ok
Fruit apple = new Apple();
fruit.add(apple); // error

下界

使用 <? super Fruit> 來表示下界.表示該集合父類至少是Fruit的集合.所以添加元素是類型安全.讀取元素是類型不安全的,只能返回Object.

List<? super Fruit> fruit = new ArrayList<Fruit>();
Fruit fruit = fruit.get(0); // error
Fruit apple = new Apple();
fruit.add(apple) // ok

無界

使用<?>來表示無界,表示該集合可以是任何類型,但是很遺憾,如果使用無界則不能添加元素,因?yàn)樵厥侨魏晤愋?在讀取時(shí)只能是Objcet,將丟失添加的類型,向下轉(zhuǎn)型有風(fēng)險(xiǎn).所以是類型不安全的.

List<?> list = new ArrayList<>();
Object o = list.get(0);
list.add(new String()); //error
list.add(new Object()); //error
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惕橙,一起剝皮案震驚了整個(gè)濱河市挂据,隨后出現(xiàn)的幾起案子食寡,更是在濱河造成了極大的恐慌,老刑警劉巖列敲,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異帖汞,居然都是意外死亡戴而,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門翩蘸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來所意,“玉大人,你說我怎么就攤上這事催首》鲇唬” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵郎任,是天一觀的道長(zhǎng)秧耗。 經(jīng)常有香客問我,道長(zhǎng)舶治,這世上最難降的妖魔是什么分井? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任车猬,我火速辦了婚禮,結(jié)果婚禮上尺锚,老公的妹妹穿的比我還像新娘珠闰。我一直安慰自己,他們只是感情好瘫辩,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布铸磅。 她就那樣靜靜地躺著,像睡著了一般杭朱。 火紅的嫁衣襯著肌膚如雪阅仔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天弧械,我揣著相機(jī)與錄音八酒,去河邊找鬼。 笑死刃唐,一個(gè)胖子當(dāng)著我的面吹牛羞迷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播画饥,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼衔瓮,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了抖甘?” 一聲冷哼從身側(cè)響起热鞍,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎衔彻,沒想到半個(gè)月后薇宠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡艰额,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年澄港,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柄沮。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡回梧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出祖搓,到底是詐尸還是另有隱情狱意,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布棕硫,位于F島的核電站髓涯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏哈扮。R本人自食惡果不足惜纬纪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一蚓再、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧包各,春花似錦摘仅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至护姆,卻和暖如春矾端,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背卵皂。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工秩铆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人灯变。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓殴玛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親添祸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子滚粟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

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