浅谈Java泛型编程原文地址:=154504我原本想全文翻译GenericsintheJavaProgrammingLanguage,但是功力不够,太耗时间。于是乎按照原文的框架,翻了一些再加上自己写的一点东西。第一次写文,如有谬误还清指出~~谢谢!浅谈Java泛型编程1引言在JDK1.5中,几个新的特征被引入Java语言。其中之一就是泛型(generics)。泛型(generics,genericity)又称为“参数类型化(parameterizedtype)”或“模板(templates)”,是和继承(inheritance)不同而互补的一种组件复用机制。继承和泛型的不同之处在于——在一个系统中,继承层次是垂直方向,从抽象到具体,而泛型是水平方向上的。当运用继承,不同的类型将拥有相同的接口,并获得了多态性;当运用泛型,将拥有许多不同的类型,并得以相同的算法作用在它们身上。因此,一般说来,当类型与实现方法无关时,使用泛型;否则,用继承。泛型技术最直接联想到的用途就是建立容器类型。下面是一个没有使用泛型技术的例子:ListmyIntList=newLinkedList();//1myIntLikst.add(newInteger(0));//2Integerx=(Integer)myIntList.iterator().next();//3显然,程序员知道究竟是什么具体类型被放进了myIntList中。但是,第3行的类型转换(cast)是必不可少的。因为编译器仅仅能保证iterator返回的是Object类型。要想保证将这个值传给一个Integer类型变量是安全的,就必须类型转换。除了使代码显得有些混乱外,类型转换更带来了运行时错误的可能性。因为程序员难免会犯错误。使用了泛型技术,程序员就可以确切地表达他们的意图,并且把myIntList限制为包含一种具体类型。下面就是前一个例子采用了泛型的代码段:ListIntegermyIntList=newLinkedListInteger();//1myIntLikst.add(newInteger(0));//2Integerx=myIntList.iterator().next();//3ListInteger指出了这不是一个随意的List,而是一个Integer的List。我们说List是一个带有类型参数的泛型接口,在这里就是指Integer。现在,我们在第1行里使用Integer作为类型参数,而不是在第3行里做类型转换。这样,在编译时刻,编译器就能够检查程序的正确性——无论何时何地,编译器都将保证myIntList的正确使用。相反地,类型转换仅仅告诉我们——在这里,程序员认为这样做是对的。采用泛型可以增强代码可读性和健壮性(robustness)。2定义泛型publicinterfaceListE{voidadd(Ex);IteratorEiterator();}publicinterfaceInteratorE{Enext();booleanhasNext();}这是一段Collection里代码,一个完整的泛型定义。尖括号里的E就是形式类型参数(formaltypeparameters)。在泛型定义中,类型参数的用法就像一般具体类型那样。在引言中,我们看到初始化了一个泛型List——ListInteger。在这里,类型参数被赋于实际类型参数(actualtypeargument)Integer。你可以想象ListInteger将获得这样的代码:publicinterfaceList{voidadd(Integerx);IteratorIntegeriterator();}和C++中对模板的处理有很大的不同,这里没有第2份副本。Java采用的是拭去法(erasure)而C++采用的是膨胀法(expansion)。一个泛型定义只被编译一次,只生成一个文件,就像一般的class和interface一样。形式类型参数可以不止1个,如:classBarE,D{……}3通配符3.1泛型和子类下面的这段代码合法么?ListStringls=newArrayListString();//1ListObjectlo=ls;//2假设这两行代码是正确的,那么下面的操作:lo.add(newObject());//3Stringstr=ls.get(0);//4将导致运行时刻错误。通过别名lo存取ls时,我们可以插入任意类型的对象——ls就不再仅仅持有String了。Java编译器消除了这种错误发生的可能性。第2行将导致编译时刻错误。一般地说,如果Foo是Bar的子类,G定义为某种泛型,那么GFoo不是GBar的子类。3.2通配符如果,我们试图使用泛型的方法编写一个打印Collection内所有元素的函数,要怎么做?voidprintCollection(CollectionObjcetc){for(Objcetobj:c){//jdk1.5中新增的语法,见5.1System.out.println(obj);}}显然这样是不行的,因为通过3.1我们可以知道——CollectionObject不是任何Collection的父类。那么,所有Collection的父类是什么?Collection?——未知类型的Collection(collectionofunknown),一个元素可以匹配为任意类型的Collection。“?”被称作通配类型。上述的代码,可以改写成这样:voidprintCollection(Collection?c){for(Objectobj:c){System.out.println(obj);}}现在,我们可以使用任意类型的Collection作为参数了。注意,在printCollection内,用Objcet类型访问c的元素是安全的,因为任何一种具体类型都是Object的子类。但是这样的操作是错误的:List?list=newArrayListString();list.add(…);//compile-timeerror!因为list被定义为List?,“?”指代了一个未知类型。list.add(…)无法保证插入的对象类型就是list实际包含的类型。唯一的例外就是null——null可以是任意类型的值。但是,通过一个List?引用,调用get()函数是可以的——即不会修改Collection的函数,就像printCollection里那样。尽管不能确定具体的类型,但是都是Object的子类。3.3受限通配符现在要创建一个简单的作图程序。我们定义了接口Shape:publicabstractclassShape{publicabstractvoiddraw();}然后定义了2个子类:publicclassCircleextendsShape{…….publicvoiddraw(){…}}publicclassRectangleextendsShape{……publicvoiddraw(){……}}很自然地,我们也会设计这样一个函数:voiddrawAll(List…shapes){for(Shapes:shapes){s.draw();}}尖括号里应该填写什么了?显然,ListShape是行不通的,这在3.1里已经说明了。List?可以,但是不好,因为如果这样使用:ListObjectlist=newArrayListObject();//1list.add(newObject());//2drawAll(list);//3编译器认为没有问题,但是运行时刻肯定报错。在drawAll里,我们实际需要的是Shape的子类,但是List?无法在编译时刻保证这一点。这里的解决方案是受限通配符(boundedwildcard)。这样做:voiddrawAll(List?extendsShapeshapes){..…}如果,再像前一个例子的第3行那样使用的话,编译器会报错。因为编译器要求shapes的每一个元素的实际类型都是Shape的子类。同使用一般通配符一样,shapes.add(…)是不允许的,因为,编译器只能保证插入的是Shape的子类对象,而不能肯定与Collection实际包含的类型是匹配的。4泛型函数考虑设计这样一个函数——把一个数组中的对象依次插入一个Collection中。我们首先这样尝试:voidaddFromArray(Object[]a,Collection?c){for(Objecto:a){c.add(o);//compile-timeerror!}}从前面的介绍中,可以明确这样是不行的。当然CollectionObject同样是错误的。解决这类问题的方法就是使用泛型函数:staticTvoidaddFromArray(T[]a,CollectionTc){for(To:a){c.add(o);}}但是必须注意,当我们执行addFromArray时,编译器将根据参数的类型检查是否安全:addFromArray(newString[10],newArrayListString());//OK!addFromArray(newString[10],newArrayListObject());//OK!addFromArray(newObject[10],newArrayListString());//compile-timeerror!addFromArray(newString[10],newArrayListInteger());//compile-timeerror!第3,4行的错误是很容易理解的,无论是把一个Object类型对象插入String的List还是把一个String插入Integer的List都是不安全的。不过,如果这样的代码是没有问题的:Tvoidfoo(Tt1,Tt2){System.out.println(t1.getClass());System.out.println(t2.getClass());}foo(newObject(),newString());//显示classjava.lang.Objectclass.lang.Stringfoo(newInteger(),newString();//显示classjava.lang.Integerclass.lang.Stringfoo(newObject[10],newArrayListString());//显示class[Ljava.lang.Object;class.util.ArrayListfoo(newString[10],newArrayListInteger());//显示class[Ljava.lang.String;class.util.ArrayList至于每一种调用T究竟是匹配了哪种类型。注意:这不是C++。经过编译,foo只生成一段代码,T就是Object。编译器只是在恰当的地方做了恰当的类型转换。4.1泛型函数和通配符的选择什么时候应当使用泛型函数,什么时候应当使用通配符呢?先看一段来自Collection里的代码:interfaceCollectionE{publicbooleancontainsAll(Collection?c);publicbooleanaddAll(Collection?extendsEc);}我们也可以用泛型函数改写:interfaceCollectionE{publicTbooleancontainsAll(CollectionTc);publicTextendsEbooleanaddAll(CollectionT