泛型主要是配合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)”。
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 }
基本定義方式
泛型的使用是很簡單的,定義就有些復(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:
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 }
2 public final C third;
3
4 public ThreeTuple( A a, B b, C c ) {
5 super( a, b );
6 third = c;
7 }
8 }
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ù)來代替。下面的代碼是對它的使用。
2 public static void main( String[] args ) {
3 new TwoTuple<String, Integer>( "Hello World", 10 );
4 new ThreeTuple<String, Integer, List<String> >( "Hello World", 10, new ArrayList<String>() );
5 }
6 }
上面是泛型類的定義,接著該介紹泛型方法了。方法更是天然就接受參數(shù)的,如果想要這些參數(shù)可以為任何類型,就要用到泛型了。泛型方法的定義方式是在返回值前列出類型參數(shù)列表(還是不能缺了這么個列表)。在使用各個tuple類時,實例化時還是很復(fù)雜,下面利用泛型方法來簡化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", 10, new ArrayList<String>() );
17 }
18 }
泛型方法和泛型類是相互獨(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)的:
- 不能對形式類型參數(shù)T使用instanceof: if ( arg instanceof T ); //Error
- 不能直接用new來生成形式類型參數(shù)T的對象:new T(); //Error
- 不能生成形式類型參數(shù)T的數(shù)組:new T[SIZE]; //Error
- 只能對T調(diào)用Object的方法
上面都是指在泛型的定義中的功能的局限,在對泛型的使用時,由于類型參數(shù)已經(jīng)具體知道了,所以也就不存在上面的問題了。
如果必須得在定義泛型時實現(xiàn)上述功能怎么辦?比如,不能新建類型參數(shù)的對象這太局限了。對于1、2、3點,可以利用type tag的方式,就是傳入類型參數(shù)的Class對象,利用Class對象的newInstance(), isInstance(), 以及Array.newInstance()來完成上述功能。下面就是如何生成對象的例子。
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)用界限的方法。界限可以有多個,但只能有一個類,其他只能是接口,而且要把類寫在最前面。
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<T 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ù)中。看下面的例子。
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的子類。可是看下面例子:
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以及它的子類的對象(比較抽象)。
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 }
對于通配符的使用,本人現(xiàn)在還不是特別理解是不是某些場合必須要用,因為其限制比較多,需要在以后的使用中進(jìn)一步加深了解,目前先搞清楚其使用方法吧。
總結(jié)
如果完全搞清楚了各個元素,泛型也不是很復(fù)雜。本文首先講了最基本的使用,到如何定義泛型,定義包含了泛型類和泛型方法。然后是介紹了泛型實現(xiàn)的原理,就是擦去法,并且是擦去到界限,這就是<T extends Bound1 & Bound2 & Bound3>這樣的形式,界限的定義的一大好處就是能使類型參數(shù)T調(diào)用界限的方法。然后就介紹了通配符“?”,它有3種不同用法,<?>, <? extends AClass>, <? super AClass>。