泛型主要是配合Collection容器使用的(由此可見容器是多么重要,因為真正的應用中都需要容器來存放大量的對象)。在沒有泛型的日子里,每個對象放到容器中后就成為了一個Object的引用了,這樣不但在拿出來的時候需要cast,最關鍵是本來容器中只想放入A類的對象,現在卻其他任何類的對象都可以放進去,在編譯時發現不了錯誤,在運行時才能發生錯誤,而且可能錯誤會隱匿很長時間,很難找到,這一點也是加入泛型的最重要的理由(TIJ4中Bruce Eckel反省道,這一觀點很可能是錯誤的,因為事實上他沒聽說過誰經常碰到此類錯誤,也沒有碰到過這種錯誤隱匿了很長時間。以前大家一直偏執的要在編譯時發現錯誤,避免到了運行時才發現錯誤的觀點很可能是沒必要的。包括checked exception)。總之,與C++的模板類似,泛型可以限定某一容器中只能加入某種類型的對象。
使用與定義
泛型的加入使得Java的語法更加復雜了,學的時候可能很容易糊里糊涂,而且即使弄得很明白,長時間不用之后又會糊涂了,我自己就是以前對泛型已經掌握的很好了,后來又忘記了。我覺得最終最重要的是要分清使用已經定義好的泛型和自己定義泛型的區別,哪些元素會在使用的時候出現,哪些會在定義的時候出現,這樣才會對增加的好幾個語法形式感到很清晰。基本使用方式
使用的時候只需要用尖括號傳入想要用的對象類型即可。傳入的對象類型稱為“類型參數(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 //當然這樣更不行
18 // List<Animal> list3 = new ArrayList<Dog>(); //Error
19 }
20 }
基本定義方式
泛型的使用是很簡單的,定義就有些復雜了。定義首先知道要有“形式類型參數”,其次要知道可以定義兩種:泛型類(Generic Class)和泛型方法(Generic Method)。TIJ4中的tuple的例子很好,可以看到有些讓人眼花繚亂的定義和繼承,讓我們快速適應泛型的語法。所謂tuple,中文翻過來就是“元組”,是數學中的一個概念,指多個值組成了一個組合,在編程語言中通常指在返回值中返回多個對象。C++中的標準模板庫包含了對tuple的支持,而以往Java的解決方法就是再定義一個類,包含需要傳回的多個對象,這樣的問題首先就是麻煩,其次在大型應用中會引起類名爆炸的問題,現在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(),返回值的類型是一個類型參數。例子展示了怎么定義泛型,就是要在類名后面添加“類型參數列表”,里面的每個類型用一個形式參數來表示,在類中就直接使用形式參數來代替。下面的代碼是對它的使用。
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 }
上面是泛型類的定義,接著該介紹泛型方法了。方法更是天然就接受參數的,如果想要這些參數可以為任何類型,就要用到泛型了。泛型方法的定義方式是在返回值前列出類型參數列表(還是不能缺了這么個列表)。在使用各個tuple類時,實例化時還是很復雜,下面利用泛型方法來簡化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 }
泛型方法和泛型類是相互獨立的,上面Tuple這個類沒有用泛型,而前面的TwoTuple, ThreeTuple等是泛型類,注意FourTuple中的getFourth()方法,返回了D,但它不是泛型方法,因為它返回的是類的類型參數(比較拗口)。區分一個方法是泛型方法還是普通類的方法,一個是看它使用的類型參數是不是屬于類的,另一個是看方法前面有沒有類型參數列表。
擦去法(Erasure)和界限(Bound)
了解了泛型的基本使用和定義的方法后,就要看看他的實現原理了。Java為了和以前的老版本兼容,采取了一種不完美的折中方式,稱為擦去法(Erasure),意思就是所有這些類型的信息都是在編譯時強制的,編譯器保證傳入了類型參數的容器不會放入非法的類型;而編譯之后,類型參數的信息就消失了,傳入的類型參數都統一變成了Object的引用,JVM看到的都只是一個一個的Object而已,和以前沒有區別,這就是所謂“擦去”的含義;在從容器中取出后,編譯器又自動進行了cast。這種實現造成了一些看似很基礎的功能無法實現,主要是和運行時類型信息相關的:
- 不能對形式類型參數T使用instanceof: if ( arg instanceof T ); //Error
- 不能直接用new來生成形式類型參數T的對象:new T(); //Error
- 不能生成形式類型參數T的數組:new T[SIZE]; //Error
- 只能對T調用Object的方法
上面都是指在泛型的定義中的功能的局限,在對泛型的使用時,由于類型參數已經具體知道了,所以也就不存在上面的問題了。
如果必須得在定義泛型時實現上述功能怎么辦?比如,不能新建類型參數的對象這太局限了。對于1、2、3點,可以利用type tag的方式,就是傳入類型參數的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)的概念,部分的解決了這一問題。就是說在定義泛型時指定一個界限,這樣擦去時就會變成了該界限的類型,而實例化類型參數就只能是這個界限的子類,這就保證了在泛型定義內部,形式類型參數一定是界限的類型,就可以調用界限的方法。界限可以有多個,但只能有一個類,其他只能是接口,而且要把類寫在最前面。
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行,調用了Animal的方法。而如果沒有設定Bound,則只能調用Object的方法,因為這時候是將類型參數擦去成為了Object,事實上Object就是這時候的界限。因此Java中泛型的原理可以用一句話表述:“擦去到界限”。
界限的意義其實是在類型參數上進行限制,從而增加表達的豐富性,但“能調用界限的方法”反而是更實際的一個效果。
通配符(Wildcards)
在“類型參數”和“界限”之后,現在又有了個新概念:“通配符”,如果不弄清楚就更加混成一團了。
通配符就是“?”,用在類型參數處,表示可以接受任何類型,如List<?>表示可以接受任何類型,Map<String, ?>的第二個參數可以接受任何類型,Map<?, ?>表示兩個參數都可以接受任何類型。
可能一開始還沒意識到這代表什么,然后再仔細一想,定義泛型的時候類型參數T(或者任何其他標識符,以下都用T來表示形式類型參數)不就是表示能接受任何類型嗎?怎么又冒出一個能表示任何類型的符號?這就是一直在強調的“定義”和“使用”的區別,原來類型參數是定義的時候使用的,而“?”是在使用的時候使用的。但還是不完全對,使用的時候應該都類型都確定了,這個“?”表示任意類型,那到底是什么類型?事實上“?”也不是在使用的最終端出現的,而是出現在一個中間的位置,比如賦值的左端,或者方法的參數中。看下面的例子。
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類不是一個泛型化的類,沒有類型參數。但它的成員變量卻是一個可以放任意類型的List,只不過實例化了以后該類型就確定了。
明白了“?”可能出現的地方以后,立刻再來些復雜的。正如對類型參數T可以進行一定的限定,“?”表示的“任意”也可以進行一定得限定,這就有了<? extends AClass>的形式,這個比較好理解,因為和<T extends AClass>一樣,表示傳入的類型參數必須是AClass的子類,兩者的邏輯是相同的,只不過用在不同地方。<T extends AClass>的用處除了表達更豐富的語義外,還有就是能用T調用AClass的方法,那<? extends AClass>也有這樣的效果嗎?
不是的,“?”不能調用方法。那目的何在?本文的第一個例子就出現了一個問題,就是類型參數用不同的類型實例化后,泛型類就不能賦值了,即使類型參數之間有繼承關系也不行,即下列語句行不通:ArrayList<Animal> list = new ArrayList<Dog>();當然此處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() ); //但任何對象都加不進去,即使是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() ) ); //也可以調用contains()!
19 System.out.println( list4.indexOf( new Dog() )); //也可以調用indexOf()!
20 }
21 }
首先,如前面所說,如果實例化的類型參數不一樣,是不能賦值的;然后,<? extends Animal>來救駕了,只要采用這種形式,就可以賦值了,如例子中第10、11行(對這一現象,TIJ4再次使用了協變(Covariant)這個詞,我覺得不太恰當,協變是指一同變化,指的是11行這種形式,ArrayList賦值給List,且類型參數分別是Dog和? extends Animal。可是第10行這種形式本質和11行是一樣的,卻沒有一同變化的情況,所以用協變稱呼這一現象不合適);第三,采用了<? extends Animal>之后,add()方法完全不能用了,連看上去本來很合理的add(new Dog())也會出現編譯錯誤;第四,可是get(), contains()和indexOf()又可以調用,那么如果說get(0)是因為用的參數是和類型參數無關的參數,因而可以調用的話,那么contains()和indexOf()又是咋回事?
看JDK的文檔可以找到第三、四點的答案,在List的定義中,添加元素的形式是add(T elem)。在使用了通配符后,由于編譯器只知道List<? extends Animal>中存的是一種Animal的子類,但卻不知道具體是哪一類,因此干脆拒絕對任意對象的添加;而contains()和indexOf()的參數是Object,因此在參數包含了“?”時可以調用。看來編譯器認為參數列表中是類型參數,如果再和“?”相關了,就是不安全的,TIJ4總結道,這需要泛型類的設計者來決定哪些方法對“?”是安全的,哪些是不安全的,安全的就以Object來作為參數,不安全的就用T作為參數。
那看起來這個<? extends Animal>還限制挺大的(這里為了說明方便,直接利用了上面的繼承結構),有沒有更寬松一點的方式?有,這就是<? super Dog>。真暈,又多出個super來,它的意思是實例化時可以用任何Dog的父類。然而注意,此處是<? super Dog>,而不是<? super Animal>,也就是在類層次上降了一層,因此表面上<? super XXX>是向著與<? extends XXX>相反的方向進行擴展,可目的卻是為了保證可以傳進去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 }
對于通配符的使用,本人現在還不是特別理解是不是某些場合必須要用,因為其限制比較多,需要在以后的使用中進一步加深了解,目前先搞清楚其使用方法吧。
總結
如果完全搞清楚了各個元素,泛型也不是很復雜。本文首先講了最基本的使用,到如何定義泛型,定義包含了泛型類和泛型方法。然后是介紹了泛型實現的原理,就是擦去法,并且是擦去到界限,這就是<T extends Bound1 & Bound2 & Bound3>這樣的形式,界限的定義的一大好處就是能使類型參數T調用界限的方法。然后就介紹了通配符“?”,它有3種不同用法,<?>, <? extends AClass>, <? super AClass>。