Java之泛型淺析(generic type)

以下都是個(gè)人理解油啤,若有錯(cuò)誤伴澄,請(qǐng)多多批評(píng)

1. 例子

先定義如下繼承關(guān)系

class C{}
class A {
        public void say() {
            System.out.println("I am A");
        }
}

class B extends A {
        public void say() {
            System.out.println("I am B");
        }
}

早期版本的Java代碼(1):

List list = new ArrayList();
list.add(123);
list.add("abc");
//o到底是什么類型真仲?不清楚....可能我們本意是放入數(shù)字喇颁,但不知被誰放入了字符串寄月,容易引發(fā)bug
Object o = list.get(0); 

之后引入了泛型,代碼(2)變成了這樣:

List<Integer> list1 = new ArrayList();
list1.add(123);
list1.add("abc"); // error ! error !
//此時(shí)o是Integer
Integer o = list1.get(0);

很多時(shí)候我們想定義一種List无牵,這種List里只能包含一個(gè)繼承體系內(nèi)的對(duì)象漾肮,比如只能包含Number,代碼(3)如下:

//list3中只能放Number的子類或Number本身
List<? extends Number> list3 = new ArrayList();
//下面這句為何會(huì)報(bào)錯(cuò)茎毁?Integer是Number的子類呀克懊。
//List<? extends Number>實(shí)際上聲明了一個(gè)list忱辅,這個(gè)list中可以放入Number的某一個(gè)子類型,Integer或Long或其他均可谭溉;但是并不能直接向里面add墙懂;
//試想,如果add(Integer)可以扮念,那么add(Long)肯定也行损搬,這樣和早期沒有泛型的代碼有何區(qū)別?
//換句話說柜与,此時(shí)list里的類型并不能確定巧勤,所以不能add具體的類型進(jìn)去.
list3.add(new Integer(1)); //error !  
//get()是可以的,因?yàn)槔锩娴念愋椭辽倌艽_定是Number的弄匕。
Number number = list3.get(0);

List<? super B> list4 = new ArrayList();
//放入B肯定是正確的
list4.add(new B());
//此時(shí)的list4可以放入B的某一個(gè)父類颅悉,但不能確定是A
list4.add(new A()); //error !
Object object = list4.get(0);//類型又不確定了

如上所述迁匠,List<? extends Number>List<? super B>都只是對(duì)類型范圍進(jìn)行了限定剩瓶,list中具體是哪種類型是不確定的。在java中城丧,若想自由的add和get延曙,list中的類型必須如代碼(2)所示的那樣,確定類型亡哄。因此枝缔,這種用法一般會(huì)在方法中來限定方法參數(shù),代碼(4)如下:

//定義一個(gè)方法, 這個(gè)方法第一個(gè)參數(shù)接收一個(gè)list磺平,這個(gè)list里要么是T魂仍,要么是T的一個(gè)父類型
private <T> void addElement(List<? super T> list, T t){
        list.add(t);
}
List<A> list2 = new ArrayList();//包含具體類型的list
//可以直接list2.add(new A())和list2.add(new B())
//確定類型的list傳入方法,
addElement(list2, new A());
addElement(list2, new B());
list2.get(0).say(); //I am A
list2.get(1).say(); //I am B

List<C> list3 = new ArrayList();
addElement(list3, new C());

2. 協(xié)變和逆變

簡單得說拣挪,協(xié)變是把子(窄)類型賦值給父(寬)類型擦酌,逆變則相反。

List list1 = new ArrayList(); // ok菠劝,多態(tài)的實(shí)質(zhì):父類的引用指向具體的一個(gè)子類實(shí)例
List<Number> list2 = new ArrayList(); // ok赊舶,此時(shí)左側(cè)可以有泛型,右側(cè)沒有泛型
List<Number> list21 = new ArrayList<Number>(); // ok

List<Number> list3 = new ArrayList<Integer>(); // error赶诊,泛型不支持協(xié)變
List<? extends Number> list4 = new ArrayList<Integer>(); // ok笼平,<? extends >使泛型也可以進(jìn)行協(xié)變

List<Integer> list5 = new ArrayList<Number>(); // error,泛型不支持逆變
List<? super Integer> list6 = new ArrayList<Number>(); // ok舔痪,<? super >使泛型支持了逆變
list6.add(new Integer(1)); // ok
list6.add(new Long(1)); // error
List<? super Number> list61 = new ArrayList<Number>(); // ok
list61.add(new Integer(1)); // ok
list61.add(new Float(1.2f)); // ok 
//寓调?super AAA,只要是在AAA的繼承圖譜中锄码,子類或者父類都可以add

3. 泛型數(shù)組

首先數(shù)組是協(xié)變的

Number[] numbers = new Integer[1]; // ok
List<String>[] list = new ArrayList[1]; // ok

但是夺英,不能創(chuàng)建泛型數(shù)組

new ArrayList<String>[1] // error
List<?>[] lsa = new List<?>[10] // ok, 這時(shí)相當(dāng)于沒有泛型

Example
下面的代碼編譯沒問題晌涕,但是運(yùn)行期報(bào)錯(cuò)了

//List<String>[] lsa = new List<String>[10]; // error.
List<String>[] lsa = new List[10]; // ok,這不是泛型數(shù)組
Object[] oa = lsa; //移花接木痛悯,運(yùn)用協(xié)變性余黎,將lsa賦給父類型
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3)); // list中放入了Integer
oa[1] = li;
String s = lsa[1].get(0); // error: ClassCastException.

上面代碼的本意是創(chuàng)建一個(gè)只能裝入String的數(shù)組,但我們使用一點(diǎn)技術(shù)手段將Integer裝入载萌,運(yùn)行時(shí)就會(huì)出錯(cuò)惧财,這就是不能創(chuàng)建泛型數(shù)組的最主要原因。

采用通配符的方式是允許的扭仁。雖然這種方式是可以的垮衷,但是需要顯示轉(zhuǎn)換類型,違背了泛型設(shè)計(jì)的初衷:添加泛型的一個(gè)重要目的就是消除這種顯示的轉(zhuǎn)換斋枢,而這種代碼又必須添加帘靡。

List<?>[] lsa = new List<?>[10]; // ok
Object[] oa = lsa;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; 
Integer i = (Integer) lsa[1].get(0); // ok知给,需要強(qiáng)制轉(zhuǎn)換
System.out.println(i);

List<String> list = new ArrayList<>();
list.add("hahaha");
oa[2] = list;
String o1 = (String) lsa[2].get(0); 
System.out.println(o1);

4. ?和T的區(qū)別

邏輯上List<?>可以看成是所有List<Integer>瓤帚,List<String>等的父類。T代表某種具體類型涩赢。很多時(shí)候二者是等價(jià)的戈次。

//下述兩個(gè)方法是相同的,不能同時(shí)出現(xiàn)在一個(gè)類中
//這種方式定義方法可以消除重復(fù)性
public void test(List<?> list){} 
public <T> void test(List<T> list){}

5. instanceof

list instanceof ArrayList<Number> // error. 這樣使用時(shí)筒扒,可能是想判斷l(xiāng)ist是否是盛著Number的Arraylist怯邪,但是Number會(huì)被擦除掉,因此不能對(duì)確切的泛型使用
list instanceof ArrayList<?> // ok花墩,相當(dāng)與list instanceof ArrayList悬秉。其目的應(yīng)該是判斷l(xiāng)ist是否是Arraylist

6. 類型擦除

我們都知道,編譯后泛型會(huì)被擦除冰蘑,那我們怎樣在運(yùn)行期獲取這些泛型信息呢和泌?
Java 引入泛型擦除的原因是避免因?yàn)橐敕盒投鴮?dǎo)致運(yùn)行時(shí)創(chuàng)建不必要的類。

(1)如下例子祠肥,兩個(gè)class相同武氓,泛型的不同并沒有導(dǎo)致生成不同的Class類。

Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2); //true

(2)定義如下類仇箱,查看一下字節(jié)碼

class Generic_2<T>{
   List<String> list = new ArrayList<String>();  //1
   List<T> list2 = new ArrayList<T>();  //2

   public void test(T t) {
       List<Integer> list3 = new ArrayList<Integer>();    //3
       list3.add(1); //4
   }
}

運(yùn)行命令:java -v Generic_2.class县恕,下面為部分信息

java.util.List<java.lang.String> list;  //1 
    descriptor: Ljava/util/List;
    flags:
    Signature: #13                          // Ljava/util/List<Ljava/lang/String;>;  //有泛型信息

  java.util.List<T> list2;  //2
    descriptor: Ljava/util/List;
    flags:
    Signature: #15                          // Ljava/util/List<TT;>;  //有泛型信息

  public void test(T);  //test方法
    descriptor: (Ljava/lang/Object;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=2
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_2
         8: aload_2
         9: iconst_1
        10: invokestatic  #6                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        13: invokeinterface #7,  2            // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z  //沒有泛型信息了
        18: pop
        19: return
      LineNumberTable:
        line 34: 0
        line 35: 8
        line 36: 19
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Lcom/young/generic/Generic_2;
            0      20     1     t   Ljava/lang/Object;
            8      12     2 list3   Ljava/util/List;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0      20     0  this   Lcom/young/generic/Generic_2<TT;>;
            0      20     1     t   TT;
            8      12     2 list3   Ljava/util/List<Ljava/lang/Integer;>;
    Signature: #32                          // (TT;)V  //有泛型信息
}

上述字節(jié)碼其實(shí)說明了泛型擦除后,其實(shí)只是擦除了Code屬性里字節(jié)碼指令相關(guān)的泛型信息剂桥,Signature這些元數(shù)據(jù)信息中其實(shí)依然保留了泛型信息忠烛,這就是我們可以通過反射獲取泛型信息的根本原因

(3)定義了幾個(gè)類來獲取泛型信息:

class A<K,V>{}
class B extends A<String, Integer>{}

interface C<T>{}
class D implements C<String>{}

public static void main(String[] args){
        //extends
        Type type = B.class.getGenericSuperclass();
        ParameterizedType parameterizedType = (ParameterizedType) type;
        for (Type type1 : parameterizedType.getActualTypeArguments()) {
            System.out.println(type1);
        }
        
        //implements
        ParameterizedType parameterizedType1 = (ParameterizedType) D.class.getGenericInterfaces()[0];//0代表只有一個(gè)接口
        for (Type type1 : parameterizedType1.getActualTypeArguments()) {
            System.out.println(type1);
        }

        //getTypeParameters()只能獲取聲明時(shí)的泛型名字
        TypeVariable<? extends Class<? extends A>>[] typeParameters = new A().getClass().getTypeParameters();
        for (TypeVariable<? extends Class<? extends A>> typeParameter : typeParameters) {
            System.out.println(typeParameter.getTypeName());
        }
}
/**output
class java.lang.String
class java.lang.Integer

class java.lang.String

K
V
**/

7. 幾個(gè)例子

Collections.java

// sort方法是既要從list中取权逗,也要把結(jié)果放入list美尸,這種即get又set的方法垒拢,必須用確定的類型T
public static <T extends Comparable<? super T>> void sort(List<T> list){...};

//fill方法只會(huì)set而不會(huì)get,所以要用super
public static <T> void fill(List<? super T> list, T obj) {...}

//這個(gè)方法只會(huì)get而不會(huì)set火惊,所以要用extends
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) {...}

本文參考了
http://swiftlet.net/archives/1950
https://www.cnblogs.com/lzq198754/p/5780426.html
https://my.oschina.net/lifany/blog/875769

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末求类,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子屹耐,更是在濱河造成了極大的恐慌尸疆,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惶岭,死亡現(xiàn)場離奇詭異寿弱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)按灶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門症革,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸯旁,你說我怎么就攤上這事噪矛。” “怎么了铺罢?”我有些...
    開封第一講書人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵艇挨,是天一觀的道長。 經(jīng)常有香客問我韭赘,道長缩滨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任泉瞻,我火速辦了婚禮脉漏,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘袖牙。我一直安慰自己侧巨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開白布贼陶。 她就那樣靜靜地躺著刃泡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碉怔。 梳的紋絲不亂的頭發(fā)上烘贴,一...
    開封第一講書人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音撮胧,去河邊找鬼桨踪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛芹啥,可吹牛的內(nèi)容都是我干的锻离。 我是一名探鬼主播铺峭,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼汽纠!你這毒婦竟也來了卫键?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤虱朵,失蹤者是張志新(化名)和其女友劉穎莉炉,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體碴犬,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡絮宁,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了服协。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片绍昂。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖偿荷,靈堂內(nèi)的尸體忽然破棺而出窘游,到底是詐尸還是另有隱情,我是刑警寧澤遭顶,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布张峰,位于F島的核電站泪蔫,受9級(jí)特大地震影響棒旗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撩荣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一铣揉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧餐曹,春花似錦逛拱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至饱狂,卻和暖如春曹步,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背休讳。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來泰國打工讲婚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人俊柔。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓筹麸,卻偏偏與公主長得像活合,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子物赶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

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