此系列文章译自SUN的泛型编程指南, 看不懂译文的请看原文
http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf
一、绪言
JDK1.5对JAVA语言进行了做了几个扩展,其中一个就是泛型。
本指南旨在介绍泛型。如果你熟悉其它语言的构造类似的东西,特别是C++的模板(template),你会很快发现它们之间的相同点及重要的不同点;如果你在其他地方没看到过类似的东西,那反而更好,那样你就可以开始全新的学习,用不着去忘掉那些(对JAVA泛型)容易产生误解的东西。
泛型允许你对类型进行抽象。最常见的例子是容器类型,比如那些在Collection层次下的类型。
下面是那类例子的典型用法:
List myIntList = new LinkedList();//1
myIntList.add(new Integer(0));//2
Integer x = (Integer) myIntList.iterator().next();//3
第3行里的强制类型转换有点烦人,程序通常都知道一个特定的链表(list)里存放的是何种类型的数据,但却一定要进行类型转换。编译器只能保证迭代器返回的是一个对象,要保证对Integer类型变量的赋值是类型安全的话,必须进行类型转换。类型转换不但会引起程序的混乱,还可能会导致运行时错误,因为程序员可能会犯错误。
如果程序员可以如实地表达他们的意图,即标记一个只能包含特定数据类型的链表,那会怎么样呢?这就是泛型背后的核心思想。下面是前面代码的泛型写法:
List myIntList = new LinkedList();//1′
myIntList.add(new Integer(0));//2′
Integer x = myIntList.iterator().next();//3′
请注意变量myIntList的类型声明,它指明了这不仅仅是一个任意的List,还是一个Integer类型的List,写作List。我们说List是一个接受类型(在这个例子是Integer)参数的泛华的接口,在创建链表对象的时候,我们也指定了一个类型参数。
另外要注意的是在第3’行的类型转换已经不见了。
现在你可能会想,我们所做的全部都是为了把混乱消除。我们没有在第3行把类型转换为Integer,而是在第1’行加了Integer类型参数;非也非也,这里面差别很大,编译器现在能够在编译期间检测程序的类型正确性。当我们把myIntList声明为类型List后,就意味着变量myIntList在何时何地的使用都是正确的,编译器保证了这一点。相反,类型转换只是告诉我们程序员认为它在程序的某个地方是正确的。
实际的结果是,程序(特别是大型的程序)的可读性和健壮性得到了提高。
二、定义简单的泛型
下面是java.util包里的List和Iterator接口定义的一个小小的引用:
public interface List{
void add(E x);
Iterator iterator();
}
public interface Iterator{
E next();
boolean hasNext();
}
除了尖括号里的东西,这里所有的都应该很熟悉了。那是List和Iterator接口的规范类型参数的声明。类型参数可以用在任何的泛型声明中,就像使用普通的类型一样(虽然有一些很重要的限制;看第7部分)。
在绪言中,我们看到了List泛型声明的调用,比如List。在调用里面(通常称为参数化类型),所有出现规范类型参数(这里是E)的全部都用实际的类型参数(这里是Integer)所代替。
你可以想象成List代表所有E都用Integer代替了的List:
public interface IntegerList{
void add(Integer x)
Iterator iterator();
}
这种想法是有所帮助的,但也会造成误解。它是有所帮助的,是因为参数化类型List有看起来像这种扩展的方法。它会造成误解,是因为泛型的声明实际上不会像那样去扩展;在源代码中、二进制文件中、硬盘和内存里,都没有代码的多个拷贝。如果你是一个C++程序员,你会明白这跟C++的模板(template)很不同。
泛型声明是一次编译,永远使用,它会变成一个单独的class文件,就像一个普通的类或接口声明。
类型参数跟用在方法或构造函数里的普通的参数类似,就像一个方法具有描述它运算用到的值的类型的规范值参一样,泛化声明具有规范类型参数。当一个方法被调用的时候,实际的参数将会被规范参数所代替而对方法求值。当一个泛化声明被调用的时候,实际类型参数将会代替规范类型参数。
命名惯例要注意的一个地方。我们建议你用一些简炼(如果可以的话只用一个字符)但却映眼的名字作为规范类型参数名。在那些名字中最后避免小写字母,这样可以很容易把规范类型参数和普通的类或接口区分开来。就像前面的例子一样,很多容器类型使用E。我们将会在后面的例子里看到其他的惯例。
三、泛型和子类化
我们来测试一下对泛型的理解,下面的代码是否正确呢?
List ls = new ArrayList();//1
List lo = ls;//2
第1行肯定是正确的,问题的难点在于第2行;这样就归结为这个问题:一个字符串(String)链表(List)是不是一个对象链表?大部分人的直觉是:“肯定了!”
那好,看一下下面这两行:
lo.add(new Object());//3
String s = ls.get(0);//4:企图把一个对象赋值给字符串!
在这里我们把ls和lo搞混淆了。我们通过别名lo来访问字符串链表ls,插入不确定对象;结果就是ls不再存储字符串,当我们尝试从里面取出数据的时候就会出错。
Java编译器当然不允许这样的事情发生了,所以第2行肯定会编译出错。
一般来说,如果Foo是Bar的子类型(子类或子接口),而G又是某个泛型声明的话,G并不是G的子类型。这可能是学习泛型的时候最难的地方,因为它与我们的深层直觉相违背。
直觉出错的问题在于它把集合里的东西假想为不会改变的,我们的本能把这些东西看作是不变的。
举个例子,假设汽车公司为人口调查局提供一份驾驶员的列表,这看上去挺合理。因为Driver是Person的一个子类,我们理所当然的以为List同样是一个List。然而实际并非如此,汽车公司提交的只是一份驾驶员登记表的副本。否则的话,人口调查局将能够把不是驾驶员的人添加到登记表中,汽车公司的驾驶员登记表将受到破坏。
为了解决这类问题,我们需要考虑一些更灵活的泛型,到现在为止碰到的规则太受约束了。
四、通配符
考虑一下写一个程序来打印一个集合对象(collection)里的所有元素。在旧版的语言里面,你可以会像下面那样写:
void printCollection(Collection c){
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++){
System.out.println(i.next());
}
}
下面尝试着用泛型(和新的for循环语法)来写:
void printCollection(Collection c){
for (Object e : c) {
System.out.println(e);
}
}
这样的问题是新版本的代码还没旧版本的代码好用。就像我们刚示范的一样,Collection并不是所有类型的集合的父类型,所以它只能接受Collection对象,而旧版的代码却可以把任何类型的集合对象作为参数来调用。
那么,什么才是所有集合类型的父类型呢?这个东西写作Collection>(读作“未知集合”),就是元素类型可以为任何类型的集合。这就是它为什么被称为“通配符类型”的原因。我们可以这样写:
void printCollection(Collection> c){
for (Object e : c) {
System.out.println(e);
}
}
现在,我们就可以以任何类型的集合对象作为参数来调用了。注意,在printCollection()方法里面,我们仍然可以从c对象中读取元素并赋予Object类型;因为无论集合里实际包含了什么类型,它肯定是对象,所以是类型安全的。但对它插入任意的对象的话则是不安全的:
Collection> c = new ArrayList();
c.add(new Object());//编译错误
由于我们并不知道c的元素类型是什么,因此我们不能对其插入对象。add()方法接受类型E,即集合的元素类型的参数。当实际的类型参数是?的时候,就代表是某未知类型。任何传递给add方法的参数,其类型必须是该未知类型的子类型。因为我们并不知道那是什么类型,所以我们传递不了任何参数。唯一的例外就是null,因为它是任何(对象)类型的成员。
另外,假设有一个List>,我们可以调用get()方法并使用其返回结果。结果类型是一个未知类型,但我们都知道它是一个对象。因此把get()方法的返回结果赋值给对象类型,或者把它作为一个对象参数传递都是类型安全的。
四、1-有界通配符
考虑一个简单的画图程序,它可以画长方形和圆等形状。为了表示这些形状,你可能会定义这样的一个类层次结构:
public abstract class Shape{
public abstract void draw(Canvas c);
}
public class Circle extends Shape{
private int x, y, radius;
public void draw(Canvas c) { … }
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) { … }
}
这些类可以在canvas上描画:
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
任何的描画通常都包括有几种形状,假设它们用一个链表来表示,那么如果在Canvas里面有一个方法来画出所有的形状的话,那将会很方便:
public void drawAll(List shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
但是现在,类型的规则说drawAll()方法只能对确切的Shape类型链表调用,比如,它不能对List类型调用该方法。那真是不幸,因为这个方法所要做的就是从链表中读取形状对象,从而对List类型对象进行调用。我们真正所想的是要让这个方法能够接受一个任何形状的类型链表:
public void drawAll(List extends Shape> shapes) { … }
这里有一个很小但很重要的不同点:我们把类型List替换为List extends Shape>。现在drawAll()方法可以接受任何Shape子类的链表,我们就可以如愿的对List调用进行啦。
List extends Shape>是一个有界通配符的例子。? 表示一个未知类型,就像我们之前所看到的通配符一样。但是,我们知道在这个例子里面这个未知类型实际是Shape的子类型(注:它可以是Shape本身,或者是它的子类,无须在字面上表明它是继承Shape类的)。我们说Shape是通配符的“上界”。
如往常一样,使用通配符带来的灵活性得要付出一定的代价;代码就是现在在方法里面不能对Shape对象插入元素。例如,下面的写法是不允许的:
public void addRectangle(List extends Shape> shapes) {
shapes.add(0, new Rectangle()); //编译错误
}
你应该可以指出为什么上面的代码是不允许的。shapes.add()方法的第二个参数的类型是 ? 继承Shape,也就是一个未知的Shape的子类型。既然我们不知道类型是什么,那么我们就不知道它是否是Rectangle的父类型了;它可能是也可能不是一个父类型,因此在那里传递一个Rectangle的对象是不安全的。
有界通配符正是需要用来处理汽车公司给人口调查局提交数据的例子方法。在我们的例子里面,我们假设数据表示为姓名(用字符串表示)对人(表示为引用类型,比如Person或它的子类型Driver等)的映射。Map是有两个类型参数的一个泛型的例子,表示键值映射。
请再一次注意规范类型参数的命名惯例:K表示键,V表示值。
public class Census {
public static void addRegistry(Map registry){ … }
}
…
Map allDrivers = …;
Census.addRegistry(allDrivers);
五、泛型方法
考虑写这样一个方法,它接收一个数组和一个集合(collection)作为参数,并把数组里的所有对象放到集合里面。
先试试这样:
static void fromArrayToCollection(Object[] a, Collection> c){
for (Object o : a){
c.add(o);//编译错误
}
}
到现在为止,你应该学会了避免把Collection作为集合参数的类型这种初学者的错误,但是你可能没意识到使用Collection>也是行不通的。重申一下,你不能把对象硬塞进一个未知类型的集合里面。
解决这类问题的方法是使用泛型方法。就像类型声明一样,方法也可以声明为泛型的,就是说,用一个或多个类型参数作为参数。
static void fromArrayToCollection(T[]a, Collection c){
for (T o : a){
c.add(o);//正确
}
}
对于集合元素的类型是数组类型的父类型,我们就可以调用这个方法。
Object[] oa = new Object[100];
Collection co = new ArrayList();
fromArrayToCollection(oa, co);// T是对象类型
String[] sa = new String[100];
Collection cs = new ArrayList();
fromArrayToCollection(sa, cs);// T是字符串类型(String)
fromArrayToCollection(sa, co);// T对象类型
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection cn = new ArrayList();
fromArrayToCollection(ia, cn);// T是Number类型
fromArrayToCollection(fa, cn);// T是Number类型
fromArrayToCollection(na, cn);// T是Number类型
fromArrayToCollection(na, co);// T是Number类型
fromArrayToCollection(na, cs);// 编译错误
请注意,我们并没有把实际的类型实参传递给泛型方法,因为编译器会根据实参的类型为我们推断出类型实参。一般地,编译器推断得到可以正确调用的最接近的(the most specific)实参类型。
现在有一个问题:我应该什么时候使用泛型方法,什么时候使用通配符类型呢?为了明白这个问题的答案,我们来看看Collection库里的几个方法:
interface Collection{
public boolean containsAll(Collection> c);
public boolean addAll(Collection extends E> c);
}
在这里我们也可以用泛型方法:
interface Collection{
public boolean containsAll(Collection c);
public extends E>boolean addAll(Collection c);
//哈哈,类型变量也可以有界!
}
但是,类型参数T在containsAll和addAll两个方法里面都只是用了一次。返回类型并不依赖于类型参数或其他传递给该方法的实参(这种是只有一个实参的简单情况)。这就告诉我们类型实参是用于多态的,它的作用只是对不同的调用可以有一系列的实际的实参类型。如果是那样的话,就应该使用通配符,通配符就是设计来支持灵活的子类型的,这也是我们这里所要表述的东西。
泛型方法允许类型参数用于表述一个或多个的实参类型对方法或及其返回类型的依赖关系。如果没有那样的一个依赖关系的话,泛型方法就不应用使用。
也有可能是一前一后一起使用泛型方法和通配符的情况,下面是Collections.copy()方法:
class Collections {
public static void copy(List dest, list< ? extends T> src) {…}
}
请注意这里两个参数类型的依赖关系,任何要从源链表src复制过来的对象都必须是对目标链表dst元素可赋值的;所以我们可以不管src的元素类型是什么,只要它是T类型的子类型。copy方法的方法头表示了使用一个类型参数,但是用通配符来作为第二个参数的元素类型的依赖关系。
我们是可以用另外一种不用通配符来写这个方法头的办法。
class Collections {
public static
vod copy(List dest, List src) { …}
}
没问题,但是当第一个类型参数用作dst的类型和批二个类型参数S的上界的时候,S它本身在src类型里只能使用一次,没有其他的东西依赖于它。这就意味
着我们可以用一个通配符来代替S了。使用通配符比声明显式的类型参数要来得清晰和简单,因此在可能的话都优先使用通配符。
当通配符用于方法头外部,作为成员变量、局部变量和数组的类型的时候,同样也有优势。请看下面的例子。
看回我们之前画图的那个问题,现在我们想要保留一份画图请求的历史记录。我们可以这样来维护这份历史记录,在Shape类里用一个静态的变量表示历史记录,然后在drawAll()方法里面把传递的实参储存到那历史记录变量里头。
static List> history = new ArrayList>();
public void drawAll(List extends Shape> shapes){
history.addLast(shapes);
for (Shape s: shapes) {
s.draw(this);
}
}
最后,我们再次留意一下使用类型参数的命名惯例。当没有更精确的类型来区分的时候,我们用T来表示类型,这是通常是在泛型方法里面的情况。如果有多个类型参数,我们可以用在字母表中与T相邻的字母来表示,比如S。如果一个泛型方法出现在一个泛型类里面,一个好的方法就是,应该避免对方法和类使用相同的类型参数以免发生混淆。这在嵌套泛型类里也一样。
六、与遗留代码的交互
到现在为止,我们所有的例子都是在一个假想的理想世界里面的,就是所有的人都在使用Java语言支持泛型的最新版本。唉,不过在现实中情况却不是那样。千百万行的代码都是用早期版本的语言来编写的,不可能把它们全部在一夜之间就转换过来。
在后面的第10部分,我们将会解决把遗留代码转为用泛型这个问题。在这部分我们要看的是比较简单的问题:遗留代码与泛型代码如何交互?这个问题分为两个部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。
六-1 在泛型代码中使用遗留代码
当你在享受在代码中使用泛型带来的好处的时候,你怎么样使用遗留代码呢?假设这样一个例子,你要使用com.Foodlibar.widgets这个包。Fooblibar.com
的人要销售一个库存控制系统,主要部分如下:
package com.Fooblibar.widgets;
public interface Part { … }
public class Inventory {
/**
*Adds a new Assembly to the inventory databse.
*The assembly is given the name name, and consists of a set
*parts specified by parts. All elements of the collection parts
*must support the Part interface.
**/
public static void addAssembly(String name, Collection parts) {…}
public static Assembly getAssembly(String name) {…}
}
public interface Assembly{
Collection getParts();//Returns a collection of Parts
}
现在,你可以用上面的API来增加新的代码,它可以很好的保证你调用参数恰当的addAssembly()方法,就是说传递的集合是一个Part类型的Collection对象,当然,泛型是最适合做这个:
package com.mycompany.inventory;
import com.Fooblibar.widgets.*;
public class Blade implements Part{
…
}
public class Guillotine implements Part {
}
public class Main {
public static void main(Sring[] args) {
Collection c = new ArrayList();
c.add(new Guillotine());
c.add(new Blade());
Inventory.addAssembly(“thingee”, c);
Collection k = Inventory.getAssembly(“thingee”).getParts();
}
}
当我们调用addAssembly方法的时候,它想要的第二个参数是Collection类型的,实参是Collection类型,但却可以,为什么呢?毕竟,大多数集合存储的都不是Part对象,所以总的来说,编译器不会知道Collection存储的是什么类型的集合。
在正规的泛型代码里面,Collection都带有类型参数。当一个像Collection这样的泛型不带类型参数使用的时候,称之为原生类型。很多人的第一直觉是Collection就是指Collection,但从我们先前所看到的可以知道,当需要的对象是Collection,而传递的却是Collection
对象的时候,是类型不安全的。确切点的说法是Collection类型表示一个未知类型的集合,就像Collection>。
稍等一下,那样做也是不正确的!考虑一下调用getParts()方法,它返回一个Collection对象,然后赋值给k,而k是Collection类型的;如果调用的结果
是返回一个Collection>的对象,这个赋值可能是错误的。
事实上,这个赋值是允许的,只是它会产生一个未检测警告。警告是需要的,因为编译器不能保证赋值的正确性。我们没有办法通过检测遗留代码中的getAssembly()方法来保证返回的集合的确是一个类型参数是Part的集合。程序里面的类型是Collection,我们可以合法的对此集合插入任何对象。
所以,这不应该是错误的吗?理论上来说,答案是:是;但实际上如果是泛型代码调用遗留代码的话,这又是允许的。对这个赋值是否可接受,得取决于程序员自己,在这个例子中赋值是安全的,因为getAssembly()方法约定是返回以Part作为类型参数的集合,尽管在类型标记中没有表明。
所以原生类型很像通配符类型,但它们没有那么严格的类型检测。这是有意设计成这样的,从而可以允许泛型代码可以与之前已有的遗留代码交互。
在泛型代码中调用遗留代码固然是危险的,一旦把泛型代码和非泛型代码混合在一起,泛型系统所提供的全部安全保证就都变得无效了。但这仍比根本不使用泛型要好,最起码你知道你的代码是一致的。
泛型代码出现的今天,仍然有很多非泛型代码,二者混合同时使用是不可避免的。如果一定要把遗留代码与泛型代码混合使用,请小心留意那些未检测警告。仔细的想想如何才能判定引发警告的代码是安全的。
如果仍然出错,代码引发的警告实际不是类型安全的,那又怎么样呢?我们会看那样的情况,接下来,我们将会部分的观察编译器的工作方式。
六-2 擦除和翻译
public String loophole(Integer x){
List ys = new LinkedList();
List xs = ys;
xs.add(x);//编译时未检测警告
return ys.iterator().next();
}
在这里我们定义了一个字符串类型的链表和一个一般的老式链表,我们先插入一个Integer对象,然后试图取出一个String对象,很明显这是错误的。如果我们忽略警告继续执行代码的话,程序将会在我们使用错误类型的地方出错。在运行时,代码执行大致如下:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return (String)ys.iterator().next();//运行时出错
}
当我们要从链表中取出一个元素,并把它当作是一个字符串对象而把它转换为String类型的时候,我们将会得到一个ClassCastException类型转换异常。在
泛型版本的loophole()方法里面发生的就是这种情况。
出现这种情况的原因是,Java的泛型是通过一个前台转换“擦除”的编译器实现的,你基本上可以认为它是一个源码对源码的翻译,这就是为何泛型版的loophole()方法转变为非泛型版本的原因。
结果是,Java虚拟机的类型安全性和完整性永远不会有问题,就算出现未检测的警告。
基本上,擦除会除去所有的泛型信息。尖括号里面的所有类型信息都会去掉,比如,参数化类型的List会转换为List。类型变量在之后使用时会被类型变量的上界(通常是Object)所替换。当最后代码不是类型正确的时候,就会加入一个适当的类型转换,就像loophole()方法的最后一行。
对“擦除”的完整描述不是本指南的范围内的内容,但前面我们所给的简单描述也差不多是那样了。了解这点很有好处,特别是当你想做诸如把现有API转为使用泛型(请看第10部分)这样复杂的东西,或者是想知道为什么它们会那样的时候。
六-3 在遗留代码中使用泛型
现在我们来看看相反的情况。假设Fooblibar.com把他们的API转换为泛型的,但有些客户还没有转换。代码就会像下面的:
package com.Fooblibar.widgets;
public interface Part { … }
publlic class Inventory {
/**
*Adds a new Assembly to the inventory database.
*The assembly is given the name name, and consists of a set
*parts specified by parts. All elements of the collection parts
*must support the Part interface.
**/
public static void addAssembly(String name, Collection parts) {…}
public static Assembly getAssembly(String name){ … }
}
public interface Assembly {
Collection getParts();//Return a collection of Parts
}
客户代码如下:
package com.mycompany.inventory;
import com.Fooblibar.widgets.*;
public class Blade implements Part {
…
}
public class Guillotine implements Part {
…
}
public class Main {
public static void main(String[] args){
Collection c = new ArrayList();
c.add(new Guillotine());
c.add(new Blade());
Inventory.addAssembly(“thingee”, c);//1: unchecked warning
Collection k = Inventory.getAssembly(“thingee”).getParts();
}
}
客户代码是在引进泛型之前写下的,但是它使用了com.Fooblibar.widgets包和集合库,两个现在都是在用泛型的。在客户代码里面使用的泛型全部都是原生类型。第1行产生一个未检测警告,因为把一个原生Collection传递给了一个需要Part类型的Collection的地方,编译器不能保证原生的Collection是一个Part类型的Collection。
不这样做的话,你也可以在编译客户代码的时候使用source 1.4这个标记来保证不会产生警告。但是这样的话你就不能使用所有JDK 1.5引入的新的语言特性。
七、晦涩难懂的部分
七-1 泛型类为所有调用所共享
下面的代码段会打印出什么呢?
List l1 = new ArrayList();
List l2 = new ArrayList();
System.out.println(l1.getClass() == l2.getClass());
你可能会说是false,但是你错了,打印的是true,因为所有泛型类的实例它们的运行时的类(run-time class)都是一样的,不管它们实际类型参数如何。泛型类之所以为泛型的,是因为它对所有可能的类型参数都有相同的行为,相同的类可以看作是有很多不同的类型。
结果就是,一个类的静态的变量和方法也共享于所有的实例中,这就是为什么不允许在静态方法或初始化部分、或者在静态变量的声明或初始化中引用类型参数。
七-2 强制类型转换和instanceof
泛型类在它所有的实例****享,就意味着判断一个实例是否是一个特别调用的泛型的实例是毫无意义的:
Collection cs = new ArrayList();
if (cs instanceof Collection) {…}//非法
类似地,像这样的强制类型转换:
Collection cstr = (Collection) cs;//未检测警告
给出了一个未检测的警告,因为这里系统在运行时并不会检测。
对于类型变量也一样:
T BadCast(T t, Object o) {
return (T) o;//未检测警告
}
类型变量不存在于运行时,这就是说它们对时间或空间的性能不会造成影响。但也因此而不能通过强制类型转换可靠地使用它们了。
七-3 数组
数组对象的组件类型可能不是一个类型变量或一个参数化类型,除非它是一个(无界的)通配符类型。你可以声明元素类型是类型变量和参数华类型的数组类型,但元素类型不能是数组对象。这自然有点郁闷,但这个限制对避免下面的情况是必要的:
List[] lsa = new List[10];//实际上是不允许的
Object o = lsa;
Object[] oa = (Object[]) o;
List li = new ArrayList();
li.add(new Integer(8));
oa[1] = li;//不合理,但可以通过运行时的赋值检测
String s = lsa[1].get(0);//运行时出错:ClassCastException异常
如果参数化类型的数组允许的话,那么上面的例子编译时就不会有未检测的警告,但在运行时出错。对于泛型编程,我们的主要设计目标是类型安全,而特别的是这个语言的设计保证了如果使用了javac -source 1.5来编译整个程序而没有未检测的警告的话,它是类型安全的。
但是你仍然会使用通配符数组,这与上面的代码相比有两个变化。首先是不使用数组对象或元素类型被参数化的数组类型,这样我们就需要在从数组中取出一个字符串的时候进行强制类型转换:
List>[] lsa = new List>[10];//没问题,无界通配符类型数组
Object o = lsa;
Object[] oa = (Object[]) o;
List li = new ArrayList();
li.add(new Integer(3));
oa[1] = li;//正确
String s = (String) lsa[1].get(0);//运行时错误,显式强制类型转换
第二个变化是,我们不创建元素类型被参数化的数组对象,但仍然使用参数化元素类型的数组类型,这是允许的,但引起现未检测警告。这样的程序实际上是不安全的,甚至最终会出错。
List[] lsa = new List>[10];//未检测警告-这是不安全的!
Object o = lsa;
Object[] oa = (Object[]) o;
List li = new ArrayList();
li.add(new Integer(3));
oa[1]=li;//正确
String s = lsa[1].get(0);//运行出错,但之前已经被警告
类似地,想创建一个元素类型是类型变量的数组对象的话,将会编译出错。
T[] makeArray(T t){
return new T[100];//错误
}
因为类型变量并不存在于运行时,所以没有办法知道实际的数组类型是什么。要突破这类限制,我们可以用第8部分说到的用类名作为运行时标记的方法。
八、 把类名作为运行时的类型标记
JDK1.5中的一个变化是java.lang.Class是泛化的,一个有趣的例子是对容器外的东西使用泛型。现在Class类有一个类型参数T,你可能会问,T代表什么啊?它就代表Class对象所表示的类型。
比如,String.class的类型是Class,Serializable.class的类型是Class,这可以提高你的反射代码中的类型安全性。
特别地,由于现在Class类中的newInstance()方法返回一个T对象,因此在通过反射创建对象的时候可以得到更精确的类型。其中一个方法就是显式传入一个factory对象,代码如下:
interface Factory {T make();}
public Collection select(Factory factory, String statement){
Collection result = new ArrayList();
//用JDBC运行SQL查询
for(/*遍历JDBC结果*/){
T item = factory.make();
/*通过SQL结果用反射和设置数据项*/
result.add(item);
}
return result;
}
你可以这样调用:
select(new Factory(){ public EmpInfo make() {
return new EmpInfo();
}}
, “selection string”);
或者声明一个EmpInfoFactory类来支持Factory接口:
class EmpInfoFactory implements Factory{
…
public EmpInfo make() { return new EmpInfo();}
}
然后这样调用:
select(getMyEmpInfoFactory(), “selection string”);
这种解决办法需要下面的其中之一:
· 在调用的地方使用详细的匿名工厂类(verbose anonymous factory classes),或者
· 为每个使用的类型声明一个工厂类,并把工厂实例传递给调用的地方,这样有点不自然。
使用类名作为一个工厂对象是非常自然的事,这样的话还可以为反射所用。现在没有泛型的代码可能写作如下:
Collection emps = sqlUtility.select(EmpInfo.class, “select * from emps”);
…
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/*用JDBC执行SQL查询*/
for(/*遍历JDBC产生的结果*/){
Object item = c.newInstance();
/*通过SQL结果用反射和设置数据项*/
result.add(item);
}
return result;
}
但是,这样并不能得到我们所希望的更精确的集合类型,现在Class是泛化的,我们可以这样写:
Collection emps = sqlUtility.select(EmpInfo.class, “select * from emps”);
…
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/*用JDBC执行SQL查询*/
for(/*遍历JDBC产生的结果*/){
T item = c.newInstance();
/*通过SQL结果用反射和设置数据项*/
result.add(item);
}
return result;
}
这样就通过类型安全的方法来得到了精确的集合类型了。
这种使用类名作为运行时类型标记的技术是一个很有用的技巧,是需要知道的。
在处理注释的新的API中也有很多类似的情况。
九 深入理解通配符
在这部分,我们将会看到通配符的几个高级用法。我们已经从示例中看到,有界通配符对从某一数据结构中读取数据是很有用的,现在来看看相反的情况,只对数据结构进行写操作。
下面的Sink接口就是这类情况的一个简单的例子:
interface Sink {
flush(T t);
}
我们可以想象在下面的示范的例子中使用它,writeAll()方法用于把coll集合里的所有元素填充(flush)到Sink接口变量snk中,并返回最后一个填充的元素。
public static T writeAll(Collection coll, Sink snk){
T last;
for (T t: coll){
last = t;
snk.flush(last);
}
return last;
}
…
Sink s;
Collection cs;
String str = writeAll(cs, s);//非法调用
如注释所注,这里对writeAll()方法的调用是非法的,因为无有效的类型参数可以引用;String和Object都不适合作为T的类型,因为Collection和Sink的元素
必须是相同类型的。
我们可以通过使用通配符来改写writeAll()的方法头来处理,如下:
public static T writeAll(Collection extends T>, Sink) {…}
…
String str = writeAll(cs, s);//调用没问题,但返回类型错误
现在调用是合法的了,但由于T的类型跟元素类型是Object的s一样,因为返回的类型也是Object,因此赋值是不正确的。解决办法是使用我们之前从未见过的一种有界通配符形式:带下界的通配符。
语法 ? super T 表示了是未知的T的父类型,这与我们之前所使用的有界(父类型:或者T类型本身,要记住的是,你类型关系是自反的)
通配符是对偶有界通配符,即用 ? extends T 表示未知的T的子类型。
public static T writeAll(Collection coll, Sink super T> snk) {…}
…
String str = writeAll(cs, s);//正确!
使用这个语法的调用是合法的,指向的类型是所期望的String类型。现在我们来看一个比较现实一点的例子,java.util.TreeSet表示元素类型是E的树形数据结构里的元素是有序的,创建一个TreeSet对象的一个方法是使用参数是Comparator对象的构造函数,Comparator对象用于对TreeSet对象里的元素进行所期望的排序进行分类。
TreeSet(Comparator c)
Comparator接口是必要的:
interface Comparator {
int compare(T fst, T snd);
}
假设我们想要创建一个TreeSet对象,并传入一下合适的Comparator对象,我们传递的Comparator是能够比较字符串的。我们可以用Comparator,但Comparator也是可以的。但是,我们不能对Comparator对象调用上面所给的构造函数,我们可以用一个下界通配符来得到我们想要的灵活性:
TreeSet(Comparator super E> c)
这样就可以使用适合的Comparator对象啦。
最后一个下界通配符的例子,我们来看看Collections.max()方法,这个方法返回作为参数传递的Collection对象中最大的元素。
现在,为了max()方法能正常运行,传递的Collection对象中的所有元素都必须是实现了Comparable接口的,还有就是,它们之间必须是可比较的。
先试一下泛化方法头的写法:
public static >
T max(Collection coll)
那样,方法就接受一个自身可比较的(comparable)某个T类型的Collection对象,并返回T类型的一个元素。这样显得太束缚了。来看看为什么,假设一个类型可以与合意的对象进行比较:
class Foo implements Comparable {…}
…
Collection cf = …;
Collectins.max(cf);//应该可以正常运行
cf里的每个对象都可以和cf里的任意其他元素进行比较,因为每个元素都是Foo的对象,而Foo对象可以与任意的对象进行比较,特别是同是Foo对象的。但是,使用上面的方法头,我们会发现这样的调用是不被接受的,指向的类型必须是Foo,但Foo并没有实现Comparable。
T对于自身的可比性不是必须的,需要的是T与其父类型是可比的,就像下面:
(实际的Collections.max()方法头在后面的第10部分将会讲得更多)
public static >
T max(Collection coll)
这样推理出来的结果基本上适用于想用Comparable来用于任意类型的用法:就是你想这样用Comparable super T>。
总的来说,如果你有一个只能一个T类型参数作为实参的API的话,你就应该用下界通配符类型(? suer T);相反,如果API只返回T对象,你就应该用上界通配符类型(? extends T),以使得你的客户的代码有更大的灵活性。
Sorry, the comment form is closed at this time.
No comments yet.