posts - 0,  comments - 1,  trackbacks - 0
    在上篇文章中总结了JDK 5.0一些新的比较小的特性,这一篇就开始复习下比较复杂的泛型(Generics),计划下一篇研究更复杂的新的线程模型。

    泛型主要是配合Collection容器使用的(由此可见容器是多么重要,因为真正的应用中都需要容器来存放大量的对象)。在没有泛型的日子里,每个对象放到容器中后就成为了一个Object的引用了,这样不但在拿出来的时候需要cast,最关键是本来容器中只想放入A类的对象,现在却其他任何类的对象都可以放进去,在编译时发现不了错误,在运行时才能发生错误,而且可能错误会隐匿很长时间,很难找到,这一点也是加入泛型的最重要的理由(TIJ4中Bruce Eckel反省道,这一观点很可能是错误的,因为事实上他没听说过谁经常碰到此类错误,也没有碰到过这种错误隐匿了很长时间。以前大家一直偏执的要在编译时发现错误,避免到了运行时才发现错误的观点很可能是没必要的。包括checked exception)。总之,与C++的模板类似,泛型可以限定某一容器中只能加入某种类型的对象。

使用与定义

    泛型的加入使得Java的语法更加复杂了,学的时候可能很容易糊里糊涂,而且即使弄得很明白,长时间不用之后又会糊涂了,我自己就是以前对泛型已经掌握的很好了,后来又忘记了。我觉得最终最重要的是要分清使用已经定义好的泛型和自己定义泛型的区别,哪些元素会在使用的时候出现,哪些会在定义的时候出现,这样才会对增加的好几个语法形式感到很清晰。

基本使用方式

    使用的时候只需要用尖括号传入想要用的对象类型即可。传入的对象类型称为“类型参数(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         //当然这样更不行
18 //        List<Animal> list3 = new ArrayList<Dog>();    //Error
19     }
20 }
    上面例子中还展示了添加了泛型后,对象的类型和继承关系(以及接口的实现)会有什么样的变化。ArrayList实现了List接口,在没有泛型的时候完全是可以赋值给它。加了泛型之后,如果用的类型参数是一样的,那仍然可以赋值;如果类型参数不一样,那么最后容器的类型就完全变化了,不能赋值,即使类型参数之间有继承关系也不行。

基本定义方式

    泛型的使用是很简单的,定义就有些复杂了。定义首先知道要有“形式类型参数”,其次要知道可以定义两种:泛型类(Generic Class)和泛型方法(Generic Method)。
    TIJ4中的tuple的例子很好,可以看到有些让人眼花缭乱的定义和继承,让我们快速适应泛型的语法。所谓tuple,中文翻过来就是“元组”,是数学中的一个概念,指多个值组成了一个组合,在编程语言中通常指在返回值中返回多个对象。C++中的标准模板库包含了对tuple的支持,而以往Java的解决方法就是再定义一个类,包含需要传回的多个对象,这样的问题首先就是麻烦,其次在大型应用中会引起类名爆炸的问题,现在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(),返回值的类型是一个类型参数。例子展示了怎么定义泛型,就是要在类名后面添加“类型参数列表”,里面的每个类型用一个形式参数来表示,在类中就直接使用形式参数来代替。下面的代码是对它的使用
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 }

    上面是泛型类的定义,接着该介绍泛型方法了。方法更是天然就接受参数的,如果想要这些参数可以为任何类型,就要用到泛型了。泛型方法的定义方式是在返回值前列出类型参数列表(还是不能缺了这么个列表)。在使用各个tuple类时,实例化时还是很复杂,下面利用泛型方法来简化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方法中,对泛型方法的使用简单很多。
   
    泛型方法和泛型类是相互独立的,上面Tuple这个类没有用泛型,而前面的TwoTuple, ThreeTuple等是泛型类,注意FourTuple中的getFourth()方法,返回了D,但它不是泛型方法,因为它返回的是类的类型参数(比较拗口)。区分一个方法是泛型方法还是普通类的方法,一个是看它使用的类型参数是不是属于类的,另一个是看方法前面有没有类型参数列表。

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

    了解了泛型的基本使用和定义的方法后,就要看看他的实现原理了。Java为了和以前的老版本兼容,采取了一种不完美的折中方式,称为擦去法(Erasure),意思就是所有这些类型的信息都是在编译时强制的,编译器保证传入了类型参数的容器不会放入非法的类型;而编译之后,类型参数的信息就消失了,传入的类型参数都统一变成了Object的引用,JVM看到的都只是一个一个的Object而已,和以前没有区别,这就是所谓“擦去”的含义;在从容器中取出后,编译器又自动进行了cast。

    这种实现造成了一些看似很基础的功能无法实现,主要是和运行时类型信息相关的:
  1. 不能对形式类型参数T使用instanceof: if ( arg instanceof T );    //Error
  2. 不能直接用new来生成形式类型参数T的对象:new T();    //Error
  3. 不能生成形式类型参数T的数组:new T[SIZE];     //Error
  4. 只能对T调用Object的方法

    上面都是指在泛型的定义中的功能的局限,在对泛型的使用时,由于类型参数已经具体知道了,所以也就不存在上面的问题了。

    如果必须得在定义泛型时实现上述功能怎么办?比如,不能新建类型参数的对象这太局限了。对于1、2、3点,可以利用type tag的方式,就是传入类型参数的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)的概念,部分的解决了这一问题。就是说在定义泛型时指定一个界限,这样擦去时就会变成了该界限的类型,而实例化类型参数就只能是这个界限的子类,这就保证了在泛型定义内部,形式类型参数一定是界限的类型,就可以调用界限的方法。界限可以有多个,但只能有一个类,其他只能是接口,而且要把类写在最前面。
 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行,调用了Animal的方法。而如果没有设定Bound,则只能调用Object的方法,因为这时候是将类型参数擦去成为了Object,事实上Object就是这时候的界限。因此Java中泛型的原理可以用一句话表述:“擦去到界限”。

    界限的意义其实是在类型参数上进行限制,从而增加表达的丰富性,但“能调用界限的方法”反而是更实际的一个效果。

 通配符(Wildcards)

    在“类型参数”和“界限”之后,现在又有了个新概念:“通配符”,如果不弄清楚就更加混成一团了。

    通配符就是“?”,用在类型参数处,表示可以接受任何类型,如List<?>表示可以接受任何类型,Map<String, ?>的第二个参数可以接受任何类型,Map<?, ?>表示两个参数都可以接受任何类型。

    可能一开始还没意识到这代表什么,然后再仔细一想,定义泛型的时候类型参数T(或者任何其他标识符,以下都用T来表示形式类型参数)不就是表示能接受任何类型吗?怎么又冒出一个能表示任何类型的符号?这就是一直在强调的“定义”和“使用”的区别,原来类型参数是定义的时候使用的,而“?”是在使用的时候使用的。但还是不完全对,使用的时候应该都类型都确定了,这个“?”表示任意类型,那到底是什么类型?事实上“?”也不是在使用的最终端出现的,而是出现在一个中间的位置,比如赋值的左端,或者方法的参数中。看下面的例子。

 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类不是一个泛型化的类,没有类型参数。但它的成员变量却是一个可以放任意类型的List,只不过实例化了以后该类型就确定了。

    明白了“?”可能出现的地方以后,立刻再来些复杂的。正如对类型参数T可以进行一定的限定,“?”表示的“任意”也可以进行一定得限定,这就有了<? extends AClass>的形式,这个比较好理解,因为和<T extends AClass>一样,表示传入的类型参数必须是AClass的子类,两者的逻辑是相同的,只不过用在不同地方。<T extends AClass>的用处除了表达更丰富的语义外,还有就是能用T调用AClass的方法,那<? extends AClass>也有这样的效果吗?

    不是的,“?”不能调用方法。那目的何在?本文的第一个例子就出现了一个问题,就是类型参数用不同的类型实例化后,泛型类就不能赋值了,即使类型参数之间有继承关系也不行,即下列语句行不通:ArrayList<Animal> list = new ArrayList<Dog>();当然此处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() );                        //但任何对象都加不进去,即使是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以及它的子类的对象(比较抽象)。

 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()了。

    对于通配符的使用,本人现在还不是特别理解是不是某些场合必须要用,因为其限制比较多,需要在以后的使用中进一步加深了解,目前先搞清楚其使用方法吧。

总结

    如果完全搞清楚了各个元素,泛型也不是很复杂。本文首先讲了最基本的使用,到如何定义泛型,定义包含了泛型类和泛型方法。然后是介绍了泛型实现的原理,就是擦去法,并且是擦去到界限,这就是<T extends Bound1 & Bound2 & Bound3>这样的形式,界限的定义的一大好处就是能使类型参数T调用界限的方法。然后就介绍了通配符“?”,它有3种不同用法,<?>, <? extends AClass>, <? super AClass>。

posted on 2008-12-08 17:24 飞马凉 阅读(174) 评论(0)  编辑  收藏

只有注册用户登录后才能发表评论。


网站导航:
 
<2024年5月>
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

留言簿

文章档案

搜索

  •  

最新评论