posts - 0,  comments - 1,  trackbacks - 0
              在上篇文章中總結(jié)了JDK 5.0一些新的比較小的特性,這一篇就開始復(fù)習(xí)下比較復(fù)雜的泛型(Generics),計劃下一篇研究更復(fù)雜的新的線程模型。

              泛型主要是配合Collection容器使用的(由此可見容器是多么重要,因為真正的應(yīng)用中都需要容器來存放大量的對象)。在沒有泛型的日子里,每個對象放到容器中后就成為了一個Object的引用了,這樣不但在拿出來的時候需要cast,最關(guān)鍵是本來容器中只想放入A類的對象,現(xiàn)在卻其他任何類的對象都可以放進(jìn)去,在編譯時發(fā)現(xiàn)不了錯誤,在運(yùn)行時才能發(fā)生錯誤,而且可能錯誤會隱匿很長時間,很難找到,這一點也是加入泛型的最重要的理由(TIJ4中Bruce Eckel反省道,這一觀點很可能是錯誤的,因為事實上他沒聽說過誰經(jīng)常碰到此類錯誤,也沒有碰到過這種錯誤隱匿了很長時間。以前大家一直偏執(zhí)的要在編譯時發(fā)現(xiàn)錯誤,避免到了運(yùn)行時才發(fā)現(xiàn)錯誤的觀點很可能是沒必要的。包括checked exception)。總之,與C++的模板類似,泛型可以限定某一容器中只能加入某種類型的對象。

          使用與定義

              泛型的加入使得Java的語法更加復(fù)雜了,學(xué)的時候可能很容易糊里糊涂,而且即使弄得很明白,長時間不用之后又會糊涂了,我自己就是以前對泛型已經(jīng)掌握的很好了,后來又忘記了。我覺得最終最重要的是要分清使用已經(jīng)定義好的泛型和自己定義泛型的區(qū)別,哪些元素會在使用的時候出現(xiàn),哪些會在定義的時候出現(xiàn),這樣才會對增加的好幾個語法形式感到很清晰。

          基本使用方式

              使用的時候只需要用尖括號傳入想要用的對象類型即可。傳入的對象類型稱為“類型參數(shù)(type parameter)”。

           1 import java.util.*;
           2 
           3 class Animal {}
           4 class Dog extends Animal {}
           5 class Cat extends Animal {}
           6 
           7 public class Test {
           8     public static void main( String[] args ) {
           9         //放的對象前后一致,則可以賦給父類的容器
          10         List<Dog> list1 = new ArrayList<Dog>();
          11         list1.add( new Dog() );
          12 //        list1.add( new Cat() );    //Can not be added
          13         
          14         //容器中放的對象不同就不能互相賦值,不是同一類型
          15 //        ArrayList<Animal> list2 = new ArrayList<Dog>();   //Error
          16         
          17         //當(dāng)然這樣更不行
          18 //        List<Animal> list3 = new ArrayList<Dog>();    //Error
          19     }
          20 }
              上面例子中還展示了添加了泛型后,對象的類型和繼承關(guān)系(以及接口的實現(xiàn))會有什么樣的變化。ArrayList實現(xiàn)了List接口,在沒有泛型的時候完全是可以賦值給它。加了泛型之后,如果用的類型參數(shù)是一樣的,那仍然可以賦值;如果類型參數(shù)不一樣,那么最后容器的類型就完全變化了,不能賦值,即使類型參數(shù)之間有繼承關(guān)系也不行。

          基本定義方式

              泛型的使用是很簡單的,定義就有些復(fù)雜了。定義首先知道要有“形式類型參數(shù)”,其次要知道可以定義兩種:泛型類(Generic Class)和泛型方法(Generic Method)。
              TIJ4中的tuple的例子很好,可以看到有些讓人眼花繚亂的定義和繼承,讓我們快速適應(yīng)泛型的語法。所謂tuple,中文翻過來就是“元組”,是數(shù)學(xué)中的一個概念,指多個值組成了一個組合,在編程語言中通常指在返回值中返回多個對象。C++中的標(biāo)準(zhǔn)模板庫包含了對tuple的支持,而以往Java的解決方法就是再定義一個類,包含需要傳回的多個對象,這樣的問題首先就是麻煩,其次在大型應(yīng)用中會引起類名爆炸的問題,現(xiàn)在Java有了泛型,自然也可以用tuple解決返回值的問題。
              2-tuple:
          1 public class TwoTuple<A, B> {
          2     public final A first;
          3     public final B second;
          4     
          5     public TwoTuple( A a, B b ) {
          6         first = a;
          7         second = b;    
          8     }
          9 }
              3-tuple:
          1 public class ThreeTuple<A, B, C> extends TwoTuple<A, B> {
          2     public final C third;
          3     
          4     public ThreeTuple( A a, B b, C c ) {
          5         super( a, b );
          6         third = c;
          7     }
          8 }
              4-tuple:
           1 public class FourTuple<A, B, C, D> extends ThreeTuple<A, B, C> {
           2     private D fourth;
           3     
           4     public FourTuple( A a, B b, C c, D d ) {
           5         super( a, b, c );
           6         fourth = d;
           7     }
           8     
           9     public D getFourth() {
          10         return fourth;
          11     }
          12 }
              
              例子中每個類里面的成員變量用public是為了引用方便,而且用了final也保證了不能被更改,其中FourTuple則特意使用了一個方法getFourth(),返回值的類型是一個類型參數(shù)。例子展示了怎么定義泛型,就是要在類名后面添加“類型參數(shù)列表”,里面的每個類型用一個形式參數(shù)來表示,在類中就直接使用形式參數(shù)來代替。下面的代碼是對它的使用
          1 public class UseTuple {
          2     public static void main( String[] args ) {
          3         new TwoTuple<String, Integer>"Hello World"10 );
          4         new ThreeTuple<String, Integer, List<String> >"Hello World"10new ArrayList<String>() );
          5     }
          6 }

              上面是泛型類的定義,接著該介紹泛型方法了。方法更是天然就接受參數(shù)的,如果想要這些參數(shù)可以為任何類型,就要用到泛型了。泛型方法的定義方式是在返回值前列出類型參數(shù)列表(還是不能缺了這么個列表)。在使用各個tuple類時,實例化時還是很復(fù)雜,下面利用泛型方法來簡化tuple的使用。
           1 public class Tuple {    
           2     public static <A, B> TwoTuple<A, B> tuple( A a, B b ) {
           3         return new TwoTuple<A, B>( a, b );
           4     }
           5     
           6     public static <A, B, C> ThreeTuple<A, B, C> tuple( A a, B b, C c ) {
           7         return new ThreeTuple<A, B, C>( a, b, c );
           8     }
           9     
          10     public static <A, B, C, D> FourTuple<A, B, C, D> tuple( A a, B b, C c, D d ) {
          11         return new FourTuple<A, B, C, D>( a, b, c, d );
          12     }
          13     
          14     public static void main( String[] args ) {
          15         tuple( "Hello World"10 );
          16         tuple( "Hello World"10new ArrayList<String>() );
          17     }
          18 }
              可以看到最后的main方法中,對泛型方法的使用簡單很多。
             
              泛型方法和泛型類是相互獨(dú)立的,上面Tuple這個類沒有用泛型,而前面的TwoTuple, ThreeTuple等是泛型類,注意FourTuple中的getFourth()方法,返回了D,但它不是泛型方法,因為它返回的是類的類型參數(shù)(比較拗口)。區(qū)分一個方法是泛型方法還是普通類的方法,一個是看它使用的類型參數(shù)是不是屬于類的,另一個是看方法前面有沒有類型參數(shù)列表。

          擦去法(Erasure)和界限(Bound)

              了解了泛型的基本使用和定義的方法后,就要看看他的實現(xiàn)原理了。Java為了和以前的老版本兼容,采取了一種不完美的折中方式,稱為擦去法(Erasure),意思就是所有這些類型的信息都是在編譯時強(qiáng)制的,編譯器保證傳入了類型參數(shù)的容器不會放入非法的類型;而編譯之后,類型參數(shù)的信息就消失了,傳入的類型參數(shù)都統(tǒng)一變成了Object的引用,JVM看到的都只是一個一個的Object而已,和以前沒有區(qū)別,這就是所謂“擦去”的含義;在從容器中取出后,編譯器又自動進(jìn)行了cast。

              這種實現(xiàn)造成了一些看似很基礎(chǔ)的功能無法實現(xiàn),主要是和運(yùn)行時類型信息相關(guān)的:
          1. 不能對形式類型參數(shù)T使用instanceof: if ( arg instanceof T );    //Error
          2. 不能直接用new來生成形式類型參數(shù)T的對象:new T();    //Error
          3. 不能生成形式類型參數(shù)T的數(shù)組:new T[SIZE];     //Error
          4. 只能對T調(diào)用Object的方法

              上面都是指在泛型的定義中的功能的局限,在對泛型的使用時,由于類型參數(shù)已經(jīng)具體知道了,所以也就不存在上面的問題了。

              如果必須得在定義泛型時實現(xiàn)上述功能怎么辦?比如,不能新建類型參數(shù)的對象這太局限了。對于1、2、3點,可以利用type tag的方式,就是傳入類型參數(shù)的Class對象,利用Class對象的newInstance(), isInstance(), 以及Array.newInstance()來完成上述功能。下面就是如何生成對象的例子。

           1 class Animal {}
           2 
           3 public class Test<T> {    
           4     private Class<T> c;
           5     private T elem;
           6     
           7     public Test(Class<T> c) {
           8         this.c = c;
           9     }    
          10     public T getElem() throws Exception {
          11         elem = c.newInstance();
          12         return elem;
          13     }    
          14     public static void main( String[] args ) throws Exception {        
          15         Test<Animal> test = new Test<Animal>( Animal.class );
          16         System.out.println( test.getElem() );
          17     }
          18 }

              對于第四點,Java引入了一個界限(Bound)的概念,部分的解決了這一問題。就是說在定義泛型時指定一個界限,這樣擦去時就會變成了該界限的類型,而實例化類型參數(shù)就只能是這個界限的子類,這就保證了在泛型定義內(nèi)部,形式類型參數(shù)一定是界限的類型,就可以調(diào)用界限的方法。界限可以有多個,但只能有一個類,其他只能是接口,而且要把類寫在最前面。
           1 class Animal {
           2     public void sayHello() {
           3         System.out.println( "Hello World" );
           4     }
           5 }
           6 class Cat extends Animal implements IntfBound1, IntfBound2 {}
           7 
           8 interface IntfBound1 {}
           9 interface IntfBound2 {}
          10 
          11 public class Test<extends Animal & IntfBound1 & IntfBound2> {
          12     private T elem;
          13     
          14     public Test( T elem ) {
          15         this.elem = elem;
          16     }
          17     
          18     public void doSomething() throws Exception {
          19         elem.sayHello();
          20     }
          21     
          22     public static void main( String[] args ) throws Exception {        
          23         Test<Cat> test = new Test<Cat>new Cat() );
          24         test.doSomething();
          25     }
          26 }

              可以看到第19行,調(diào)用了Animal的方法。而如果沒有設(shè)定Bound,則只能調(diào)用Object的方法,因為這時候是將類型參數(shù)擦去成為了Object,事實上Object就是這時候的界限。因此Java中泛型的原理可以用一句話表述:“擦去到界限”。

              界限的意義其實是在類型參數(shù)上進(jìn)行限制,從而增加表達(dá)的豐富性,但“能調(diào)用界限的方法”反而是更實際的一個效果。

           通配符(Wildcards)

              在“類型參數(shù)”和“界限”之后,現(xiàn)在又有了個新概念:“通配符”,如果不弄清楚就更加混成一團(tuán)了。

              通配符就是“?”,用在類型參數(shù)處,表示可以接受任何類型,如List<?>表示可以接受任何類型,Map<String, ?>的第二個參數(shù)可以接受任何類型,Map<?, ?>表示兩個參數(shù)都可以接受任何類型。

              可能一開始還沒意識到這代表什么,然后再仔細(xì)一想,定義泛型的時候類型參數(shù)T(或者任何其他標(biāo)識符,以下都用T來表示形式類型參數(shù))不就是表示能接受任何類型嗎?怎么又冒出一個能表示任何類型的符號?這就是一直在強(qiáng)調(diào)的“定義”和“使用”的區(qū)別,原來類型參數(shù)是定義的時候使用的,而“?”是在使用的時候使用的。但還是不完全對,使用的時候應(yīng)該都類型都確定了,這個“?”表示任意類型,那到底是什么類型?事實上“?”也不是在使用的最終端出現(xiàn)的,而是出現(xiàn)在一個中間的位置,比如賦值的左端,或者方法的參數(shù)中。看下面的例子。

           1 public class Test {
           2     private List<?> list;
           3     
           4     public void setList( List<?> list ) {     //可接受任意類型
           5         this.list = list;
           6     }
           7     
           8     public static void main( String[] args ) throws Exception {
           9         List<?> list = new ArrayList<String>();      //右值實例化,左值接受任意類型
          10         new Test().setList( list );
          11     }
          12 }

              該例子中Test類不是一個泛型化的類,沒有類型參數(shù)。但它的成員變量卻是一個可以放任意類型的List,只不過實例化了以后該類型就確定了。

              明白了“?”可能出現(xiàn)的地方以后,立刻再來些復(fù)雜的。正如對類型參數(shù)T可以進(jìn)行一定的限定,“?”表示的“任意”也可以進(jìn)行一定得限定,這就有了<? extends AClass>的形式,這個比較好理解,因為和<T extends AClass>一樣,表示傳入的類型參數(shù)必須是AClass的子類,兩者的邏輯是相同的,只不過用在不同地方。<T extends AClass>的用處除了表達(dá)更豐富的語義外,還有就是能用T調(diào)用AClass的方法,那<? extends AClass>也有這樣的效果嗎?

              不是的,“?”不能調(diào)用方法。那目的何在?本文的第一個例子就出現(xiàn)了一個問題,就是類型參數(shù)用不同的類型實例化后,泛型類就不能賦值了,即使類型參數(shù)之間有繼承關(guān)系也不行,即下列語句行不通:ArrayList<Animal> list = new ArrayList<Dog>();當(dāng)然此處Dog是Animal的子類。可是看下面例子:

           1 import java.util.*;
           2 
           3 class Animal {}
           4 class Cat extends Animal {}
           5 class Dog extends Animal {}
           6 
           7 public class Test {
           8     public static void main( String[] args ) throws Exception {
           9 //        ArrayList<Animal> list1 = new ArrayList<Dog>();        //錯誤!
          10         ArrayList<? extends Animal> list2 = new ArrayList<Dog>();  //可以接受!
          11         List<? extends Animal> list3 = new ArrayList<Dog>();   //也可以
          12 //        list3.add( new Cat() );                        //但任何對象都加不進(jìn)去,即使是Dog,Animal也不行
          13 //        list3.add( new Animal() );
          14 //        list3.add( new Dog() );
          15         List<? extends Animal> list4 = Arrays.asList( new Dog() );  //由于完全無法add(),用這種方法使它初始就包含有對象
          16 //        list4.add( new Dog() );                         //同理,仍然不能add()
          17         Animal a = list4.get( 0 );                        //卻可以get()
          18         System.out.println( list4.contains( new Dog() ) );   //也可以調(diào)用contains()!
          19         System.out.println( list4.indexOf( new Dog() ));     //也可以調(diào)用indexOf()!
          20     }
          21 }

              首先,如前面所說,如果實例化的類型參數(shù)不一樣,是不能賦值的;然后,<? extends Animal>來救駕了,只要采用這種形式,就可以賦值了,如例子中第10、11行(對這一現(xiàn)象,TIJ4再次使用了協(xié)變(Covariant)這個詞,我覺得不太恰當(dāng),協(xié)變是指一同變化,指的是11行這種形式,ArrayList賦值給List,且類型參數(shù)分別是Dog和? extends Animal。可是第10行這種形式本質(zhì)和11行是一樣的,卻沒有一同變化的情況,所以用協(xié)變稱呼這一現(xiàn)象不合適);第三,采用了<? extends Animal>之后,add()方法完全不能用了,連看上去本來很合理的add(new Dog())也會出現(xiàn)編譯錯誤;第四,可是get(), contains()和indexOf()又可以調(diào)用,那么如果說get(0)是因為用的參數(shù)是和類型參數(shù)無關(guān)的參數(shù),因而可以調(diào)用的話,那么contains()和indexOf()又是咋回事?

              看JDK的文檔可以找到第三、四點的答案,在List的定義中,添加元素的形式是add(T elem)。在使用了通配符后,由于編譯器只知道List<? extends Animal>中存的是一種Animal的子類,但卻不知道具體是哪一類,因此干脆拒絕對任意對象的添加;而contains()和indexOf()的參數(shù)是Object,因此在參數(shù)包含了“?”時可以調(diào)用。看來編譯器認(rèn)為參數(shù)列表中是類型參數(shù),如果再和“?”相關(guān)了,就是不安全的,TIJ4總結(jié)道,這需要泛型類的設(shè)計者來決定哪些方法對“?”是安全的,哪些是不安全的,安全的就以O(shè)bject來作為參數(shù),不安全的就用T作為參數(shù)。

              那看起來這個<? extends Animal>還限制挺大的(這里為了說明方便,直接利用了上面的繼承結(jié)構(gòu)),有沒有更寬松一點的方式?有,這就是<? super Dog>。真暈,又多出個super來,它的意思是實例化時可以用任何Dog的父類。然而注意,此處是<? super Dog>,而不是<? super Animal>,也就是在類層次上降了一層,因此表面上<? super XXX>是向著與<? extends XXX>相反的方向進(jìn)行擴(kuò)展,可目的卻是為了保證可以傳進(jìn)去XXX以及它的子類的對象(比較抽象)。

           1 class Animal {}
           2 class Cat extends Animal {}
           3 class Dog extends Animal {}
           4 class BigDog extends Dog {}
           5 
           6 public class Test {
           7     public static void main( String[] args ) throws Exception {
           8         List<? super Dog> list = new ArrayList<Animal>();   
           9         list.add( new Dog() );
          10         list.add( new BigDog() );
          11         Dog d = list.get( 0 );
          12         System.out.println( d );
          13 //        BigDog bd = list.get( 1 );   //錯誤!
          14         Dog bd = list.get( 1 );
          15         System.out.println( bd );
          16     }
          17 }
              上面例子中,就既可以add(),又可以get()了。

              對于通配符的使用,本人現(xiàn)在還不是特別理解是不是某些場合必須要用,因為其限制比較多,需要在以后的使用中進(jìn)一步加深了解,目前先搞清楚其使用方法吧。

          總結(jié)

              如果完全搞清楚了各個元素,泛型也不是很復(fù)雜。本文首先講了最基本的使用,到如何定義泛型,定義包含了泛型類和泛型方法。然后是介紹了泛型實現(xiàn)的原理,就是擦去法,并且是擦去到界限,這就是<T extends Bound1 & Bound2 & Bound3>這樣的形式,界限的定義的一大好處就是能使類型參數(shù)T調(diào)用界限的方法。然后就介紹了通配符“?”,它有3種不同用法,<?>, <? extends AClass>, <? super AClass>。

          posted on 2008-12-08 17:24 飛馬涼 閱讀(186) 評論(0)  編輯  收藏

          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導(dǎo)航:
           
          <2025年8月>
          272829303112
          3456789
          10111213141516
          17181920212223
          24252627282930
          31123456

          留言簿

          文章檔案

          搜索

          •  

          最新評論

          主站蜘蛛池模板: 万源市| 黎川县| 禄丰县| 林州市| 白玉县| 合川市| 长泰县| 从江县| 当阳市| 昂仁县| 衡东县| 玉龙| 漳平市| 稻城县| 海晏县| 宁强县| 钦州市| 武汉市| 来宾市| 孟州市| 临武县| 崇明县| 鲁山县| 兴和县| 漳平市| 高雄县| 拉萨市| 柳州市| 忻城县| 旌德县| 梅河口市| 肥城市| 萨嘎县| 凭祥市| 山西省| 漳州市| 揭阳市| 拜城县| 濉溪县| 桂平市| 江城|