在上篇文章中总结了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", 10, new 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", 10, new ArrayList<String>() );
17 }
18 }
可以看到最后的main方法中,对泛型方法的使用简单很多。
泛型方法和泛型类是相互独立的,上面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()来完成上述功能。下面就是如何生成对象的例子。
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<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来表示形式类型参数)不就是表示能接受任何类型吗?怎么又冒出一个能表示任何类型的符号?这就是一直在强调的“定义”和“使用”的区别,原来类型参数是定义的时候使用的,而“?”是在使用的时候使用的。但还是不完全对,使用的时候应该都类型都确定了,这个“?”表示任意类型,那到底是什么类型?事实上“?”也不是在使用的最终端出现的,而是出现在一个中间的位置,比如赋值的左端,或者方法的参数中。看下面的例子。
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
飞马凉 阅读(178)
评论(0) 编辑 收藏