Java Tutorials -- Generics
Java Generics伴随JDK 5.0发布到现在已经超过2年半了,但目前还没有被"非常广泛"地应用,我也一直没有进行过系统的学习。最近使用Thinking in Java(4th)和Java Tutorials对泛型进行了专门的学习。本文是对Java Tutorials中Generics一章的翻译。其实关于Java Generics的文章已是汗牛充栋,之所以将这篇译文放在此处,也算是对自己学习的一种鼓励吧。该文的读者应该只有我一人,但仍然希望对其他朋友有所助益。(2007.07.10最后更新)
1 介绍
JDK 5.0引进了几种Java程序设计语言的新扩展。其中之一,就是对泛型的引入。
本次体验只是对泛型的介绍。你可能通过其它的语言,特别是C++ Template,已经对泛型的结构有些熟悉了。如果是这样的话,你将看到它们的相似点和重要的不同点。如果你对从别处看到的这种似曾相识的结构不熟悉的话,那就更好了,你可以从头开始,以避免不得不忘却一些误解。
泛型允许你抽象出类型。最普通的例子就是容器类型,如集合框架(Collection)中的那些类。
下面是一个此类特性的典型使用:
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
第三行的强制类型转换有点烦人。基本上,程序员知道到底是什么类型的数据被放到这个特定的List中了。然而,这个强制类型转换是必需的。编译器只能保证迭代器将返回的是一个对象。为了确保一个类型为Integer的变量x是类型安全的,这个强制类型转换是需要的。
当然,这个强制类型转换并不会造成混乱。它仍然可能会造成一个运行时错误,可能是由程序员的失误而产生的。
那么程序员如何才能准确地表达他们的本意,使得一个List被限制为只能包含某个特定类型的数据呢?这正是泛型背后的核心思想。下面的程序片断是前述例子的泛型版:
List<Integer> myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'
注意变量myIntList的类型声明。它不是指定了一个任意的List,而是指定了一个Integer对象的List,写作List<Integer>。我们说,List是一个拥有类型参数,在此处就是Integer,的泛型接口。当创建这个List对象时,我们也指定了一个类型参数。
再次注意,原来行3的的强制类型转换已经不需要了。
现在你可能会想我们所已经完成的就是移除了那个混乱(强制类型转换)。我们在行1处就使Integer成为一个类型参数,而不是在行3处进行强制类型转换。这儿就有一个很大的不同。在编译时,编译器就能够检查程序中的类型是否正确。当我们说myIntList在声明时使用了类型List<Integer>,那么就是告诉我们myIntList变量在任何时间和任何地点所包含的类型必须是Integer,并且编译器会确保这一点。相反地,强制类型转换只是告诉我们在代码中的某个独立的地方程序员所期望的情况而以。
在实际情况下,特别是在大型应用中,泛型可以提高程序的可读性和鲁棒性。
2 定义简单的泛型
下面是java.util包中List和Iterator接口定义的简短摘要:
public interface List <E>{
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E>{
E next();
boolean hasNext();
}
除了角括号中的内容,我们对这段代码应该比较熟悉了。这些是List和Iterator接口的形式类型参数的声明。
类型参数的使用可以贯穿于整个泛型声明,用在那些你以后想使用普通类型的地方(但有一些重要的约束,详见"良好的打印"一节)。
在"介绍"一节中,我们知道了使用了泛型类型声明的List接口的调用方法,如List<Integer>。在这个调用(一般就是调用一个参数化的类型)中,所有形式类型参数(即此处的E)出现的地方都被实际的类型参数(即此处的Integer)替换了。
你可能会想像List<Integer>表示一种由Integer统一地代替E之后的新的List版本:
public interface IntegerList {
void add(Integer x);
Iterator<Integer> iterator();
}
这种直觉是有助益的,但那也是误解。
说它是有助益的,是因为参数类型List<Integer>实际上所使用的方法看起来就是像那种扩展。
说它是误解,是因为泛型的声明确实没有用那种方式进行扩展。并不存在那些代码的多个复本,在源文件中、二进制文件中、硬盘中、内存中都没有这些复本。如果你是C++程序员,你将会发现这与C++ Template非常的不同。
泛型类型的声明绝对只会被编译一次,然后进入一个class文件中,就像一个普通的类或接口声明一样。
类型参数类似于方法或构造器中的普通参数。它非常像一个方法拥有一个形式值参数,这个参数描述了可以出现在该处的值的类型,泛型声明也有一个形式类型参数。当一个方法被调用时,一个真实的的参数会替换形式参数,然后这个方法会进行评估。当一个泛型声明被调用时,一个真实的类型参数也会替代形式类型参数。
需要注重一个命名规范。我们推荐你使用叫起来尽量精简的(如果可能的话,最好是单个字母)的名字作为形式类型参数。最好避免使用小写字母,这样就可以很容易地从普通的类和接口中区分出形式类型参数。如上述例子中,很多容器类型使用E代表容器中的元素(element)。
3 泛型与子类
让我们测试一下你对泛型的理解。下面的代码片断是合法的吗?
List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2
第一行肯定是合法的。这个问题狡猾的部分是在第二行。这个问题可归结为:一个String对象的List也是Object对象的List吗?大部分人都会本能的回答到,是的!
那好,来看看下面几行:
lo.add(new Object()); // 3
String s = ls.get(0); // 4: 试图将一个Object对象赋值给一个String变量!
此处我们已经别名化了ls和lo。通过别名lo访问ls,一个String对象的List,我们可以向其中插入任意对象。但ls不能包含除String对象外的其它对象,则当我们试图从中获得些什么(Object对象)时,我们会感到非常的惊讶。
译者:上面这段话的意思是说,如果上述4行代码都成立的话,那么就会使我们感到很惊讶、很困惑。lo的类型是List<Object>,那么可以放入任意的Object到这个List中;而ls的类型是List<String>,即只能放入String对象。但lo引用的对象实际上是ArrayList<String>的对象,即只能存放String对象,所以上面的例子会使人感到很困惑。
当然,Java编译器会阻止这一切的发生--第二行将会导致一个编译时错误。
一般地,如果Foo是Bar的子类型(子类或子接口),且G是某个泛型类型声明,那么G<Foo>并不是G<Bar>的子类型。这可能是当你学习泛型时所遇到的最困难的问题,因为这违反了我们根深蒂固的直觉。
我们不能假设集成对象们不会改变。我们的直觉可能会导致我们静态地思考这些问题。
例如,如果机动车管理部(Department of Motor Vehicles, DMV)向人口调查局(Census Bureau)提交了一组司机的名单,这会被看成是合理的,因为我们认为List<Driver>是List<Person>的子类型(假设Driver是Person的子类型)。实际上被提交的只是司机注册表的副本。否则,人口调查局也可以把那些不是司机的人也加入到这个名单 (List)中,这就会破坏DMV的记录。
为了应对这种情况,有必要考虑更为弹性的泛型类型。我们到目前为止所看到的规则实在是太具限制性了。
4 通配符
考虑这样一个问题,写一个程序打印出一个集合对象中的所有元素。下面的程序可能是你用老版Java语言所写的:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
这儿有一个不成熟的对泛型应用的尝试(并且使用了新的foreach循环语法):
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
这个问题就是新版的程序并不比旧版的程序更有用。反之,旧版的程序能够作为参数被任何类型的集合对象调用,新版的程序只能用于Collection<Object>,而这种情况已经被我们证明了,它并不是所有集合类型的超类。
那么什么才是所有集合对象的超类呢?它应该写作Collection<?>(叫作"collection of unknow,未知的集合"),这种集合类型的元素才可能配置任何类型。很明显,它被称作通配符类型。我们可以这样写:
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
然后我们就可以用任何集合类型来调用这个方法了。注意printCollection方法的内部,我们仍然可以从c中读取它的元素,并可将这些元素赋值给Object类型的变量。
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error
由于不知道c中元素的类型是什么,我们不能向它里面添加元素。add方法接受类型E的参数,即这个集合对象元素的类型。当然实际的类型参数是"?"时,它表示某个未知的类型。任何我们要添加入的参数都将不得不是未知类型的子类型。由于我们不知道这个类型是什么,所以我们不能传入任何类型。唯一的例外是 "null",null是每个类型的成员(译者:null是每种类型的子类型。)。
另一方面,给出一个List<?>,我们就能调用get方法并使用得到的结果。所得结果的类型是未知的,但我们总可以知道它是一个 Object对象。因此将由get方法得到的结果赋予一个Object类型的变量,或是将它作为一个参数传入一个期望获得Object类型对象的地方,都是完全的。
有边界的通配符
考虑这样的一个简单的绘图程序,它可以绘制诸如矩形和环形之类的形状。为了使用程序来描述这些形状,你可能是会下面那样定义一组类:
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);
}
}
任何绘制动作通常都会包含一组形状。假设使用List来表示它们,那么为方便起见,Canvas需要有一个方法去绘制所有的形状:
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
现在,规则要求drawAll方法只能用于仅包含Shape对象的List,例如它不能用于List<Circle>。但不幸的是,由于所有的方法所做的只是从List中读取Shape对象,所以它也需要能用于List<Circle>。我们所想要的就是这个方法能够接受所有的 Shape类型。
public void drawAll(List<? extends Shape> shapes) {
...
}
这儿是一个很小但很重要的区别:我们已经用List<? extends Shape>代替了List<Shape>。现在,drawAll方法就可以接受Shape的任何子类对象的List了。
List<? extends Shape>就是有边界的通配符的一个例子。问号(?)代表未知类型,就如我们之前所看到的这个通配符一样。然而,在这个例子中,我们这个未知类型实际上是Shape类的子类。(注:它可以是Shape类型本身;无需按字面上的意义一定说是Shape子类)。
一般地,在使用通配符时要付出一些弹性方面的代价。这个代价就是,马上向该方法体中写入Shape类型的对象是非法的。例如,下面的代码是不被允许的:
public void addRectangle(List<? extends Shape> shapes) {
shapes.add(0, new Rectangle()); // Compile-time error!
}
你应该会指出为什么上面的代码是不能被接受的。shaps.add的第二个参数是"? extends Shape"--一个未知的Shape子类,由于我们不知道它会是哪个Shape类型,不知道它的超类是否就是Rectangle;它可能是,也可能不是 Rectangle的超类,所以当传递一个Rectangle对象,并不安全。
有边界的通配符正是上一节中DMV向人口调查局提交数据的例子所需要的。我们的例子假设那些数据是由姓名(用字符串表示)到人(用Person或其子类,如 Driver,的引用类型表示)的映射表示。Map<K, V>是包含两个类型参数的例子,这两个类型参数分别表示映射中的键与值。
再次注意形式类型参数的命名规范--K代表键,V代表值。
public class Census {
public static void addRegistry(Map<String, ? extends Person> registry) {
}
...
Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);
5 泛型方法
考虑写一个方法,它包含一个Object数据和一个集合对象,它的作用是将数组中的对象全部插入到集合对象中。下面是第一次尝试:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // Compile time error
}
}
到现在为此,你要学会避免新手所犯的错误--尝试将Collection<Object>作为这个集合的类型参数。你可能认识或没认识到使用 Collection<?>也不能完成工作。回忆一下,你不能将对象挤入一个未知类型的集合对象中。
处理这些问题的方法是使用泛型方法。就像类型的声明一样,方法的声明也可以泛型化--即,用一个或多个参数去参数化这个方法。
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // Correct
}
}
我们能够调用任意类型的集合对象中的方法,只要这个集合对象中的元素是数组类型中元素的超类型。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
fromArrayToCollection(oa, co); // T inferred to be Object
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
fromArrayToCollection(sa, cs); // T inferred to be String
fromArrayToCollection(sa, co); // T inferred to be Object
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
fromArrayToCollection(ia, cn); // T inferred to be Number
fromArrayToCollection(fa, cn); // T inferred to be Number
fromArrayToCollection(na, cn); // T inferred to be Number
fromArrayToCollection(na, co); // T inferred to be Object
fromArrayToCollection(na, cs); // compile-time error
注意我们并不需要传递一个确切的类型给泛型方法。编译器会根据准确的参数的类型帮我们推断出实际类型参数。编译器通常会推断出大部分的特定类型参数,这就使得对方法的调用是类型正确的。
产生了一个问题:什么时候我应该使用泛型方法,什么时候我应用使用通配符类型?为了理解答案,让我们测试一些集合框架类库中的方法:
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
我们可能使用下面的泛型方法替换上面的程序:
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
// Hey, type variables can have bounds too!
}
然而,在两个containAll和addAll方法中,类型参数T只被使用了一次。返回类型既不依赖类型参数,也不需要传递其它的参数给这个方法(在本例中,只不过是一个实参罢了)。这就告诉我们该实参将用于多态;它的仅有的作用就是允许该方法的多种不同的实参能够应用于不同的调用点。
泛型方法允许类型参数用于描述一个或多个实参的类型对于该方法和/或它的返回值之间依赖关系。如果没有这种依赖关系,那么就不应该使用泛型方法。
一前一后的使用泛型方法和通配符是可能的,下面的方法Collections.copy()就表现了这一点: class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
注意这两个参数的类型之间的依赖关系。任何复制于源表scr的对象对于目标表dest中元素的类型T都必须是可赋值的。所以src元素的类型肯定是T的任何子类型--我们不用关心这些。复制方法的签名使用一个类型参数描述了这种依赖关系,但将通配符用于第二个参数中元素的类型。
我们也可以使用另一种方法来书写这个方法的签名,这种方法完全不需要使用通配符:
class Collections {
public static <T, S extends T>
void copy(List<T> dest, List<S> src) {
...
}
这很好,但当第一个类型参数在类型dest和第二个类型的限度中都使用了时,S它那本身只被使用了一次,就是在src的类型中--没任何其它的东西再依赖于它了。这就是一个我们要以使用通配符替换S的一个信号。使用通配符比显示的声明类型变量更加清晰、更加精确,所以在任何可能的时候通配符是首选。
通配符也有它的优点,它可以被用于方法签名的外面,以作为字段的类型,局部变量或数组。下面就是这样的一个例子。
回到我们绘制形状的那个例子,假设我们想维护一个绘制形状请求的历史记录。我们可以将这个历史记录维护在类Shape内部的一个静态变量,让drawAll方法将它自己获得的实参(即要求绘制的形状)加入历史字段中。
static List<List<? extends Shape>> history =
new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) {
history.addLast(shapes);
for (Shape s: shapes) {
s.draw(this);
}
}
最后,仍然让我们再次注意类型变量的命名规范。我们一般使用T表示类型,只要无需再区别任何其它的特定类型。这种情况经常用于泛型方法中。如果有多个类型参数,我可以使字母表中邻近T的其它字母,例如S。如果在一个泛型类中有一个泛型方法,那么为了避免混淆,一个好的习惯是不要使泛型类和泛型方法有相同名字的类型参数。这也适用于嵌套泛型类。
6 与遗留代码交互
到现在为止,我们的例子是假设处于一种理想的状况,即每个人都在使用Java程序设计语言的支持泛型的最新版。
唉,但现实并非如此。数以百万行计的代码是用Java语言的早期版本写的,而且也不可能在一夜之间就将它们转换到新版中。
稍后,在"使用泛型转化遗留代码"这一节中,我们将解决将你的旧代码转换到使用泛型这个问题。在本节,我们将关注一个简单的问题:遗留代码与泛型代码之间如何交互?这个问题含有两个部分:在泛型代码内部使用遗留代码;在遗留代码内部使用泛型代码。
作为一个例子,假设你想使用包com.Fooblibar.widgets。分支Fooblibar.com*商用在一个资产管理系统中,这个系统的精华如下所示:
package com.Fooblibar.widgets;
public interface Part { ...}
public 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(); // Returns a collection of Parts
}
现在,你要添加一些新的代码并使用上述API。比较好的是,要确保你一直能够使用适当的实参去调用addAssembly方法--即,你传入的集合对象必须是装有Part对象的集合对象。当然,泛型最适合做这些了:
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<Part> c = new ArrayList<Part>();
c.add(new Guillotine()) ;
c.add(new Blade());
Inventory.addAssembly("thingee", c);
Collection<Part> k = Inventory.getAssembly("thingee").getParts();
}
}
当我们调用addAssembly方法时,该方法希望第二个参数的类型是Collection。该参数的实际类型是Collection< Part>。这是正确的,但是什么呢?毕竟,大部分的Collection是不能包含Part对象的,因为一般来说,编译器无法知道该 Collection所表示的是哪种对象的集合对象。
在合适的泛型代码中,Collection将一直跟随着一个类型参数。当一个像Collection这样的泛型类型在被使用时没有提供类型参数,就被称之为原生类型(Raw Type)。
大多数人的每一直觉认为Collection就是Collection<Object>。然而,按我们之前所说的,在需要Collection<Object>的地方使用Collection<Part>并不是安全的。
但请等等,那也不对!想想对getParts对象的调用,它要返回一个Collection对象(实际上是一个引用变量)。然后这个对象被赋于变量k,k是 Collection<Part>类型。如果调用该方法而返回的结果是一个Collection<?>对象,该赋值操作也将产生错误。
事实上,该赋值操作是合法的,它会生产一个未检查的警告。这个警告是必要的,因为事实上编译器并不能保证它的正确性。我们没办法检查 getAssembly方法中的遗留代码以保证返回的集合对象Part对象的集合。被用于该代码的类型是Collection,能够合法的向这种 Collection中插入任何类型的对象。
那么这还应该是一个错误吗?就理论上而言,是的;但就实际上而言,如果泛型代码是为了调用遗留代码,那么就不得不允许了。对于你,一个程序员,会对这种情况感到满意的,赋值是安全的,因为getAssermbly方法的规则告诉我们它返回返回的是 Part对象的Collection,即使该方法的签名并没有表明这一点。
所以原生类型非常像通配符类型,但它们不会被做严格的类型检查。这是经过深思熟虑之后的结果,是为了允许泛型代码能够与之前已存在的代码交互使用。
用泛型代码调用遗留代码是天生危险的;一旦你在泛型代码中混合了非泛型的遗留代码,那么泛型类型系统通常都无法提供完全的保证。然而,这仍然比你不使用泛型要好些。至少你知道最终这些代码是一致的。
碰到那儿已经有了很多的非泛型代码,然后又有了泛型代码的时候,那么无法避免的情况就是不得不混合它们。
如果你发现你必须混合使用遗留代码和泛型代码,请密切注意未检查的警告。要谨慎地思考你如何再才能证明那些被给出了危险警告的代码是安全的。
当你继续犯错误,且代码造成的警告确实不是类型安全的,什么事情将发生呢?让我们看看这样的一种情况。在这个处理过程中,我们将观察编译器所做的事情。
擦除和翻译
public String loophole(Integer x) {
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x); // Compile-time unchecked warning
return ys.iterator().next();
}
此处,我们已经别名化了String的List和一个普通的老版的List。我们向这个List xs插入一个Integer对象,并试图抽取一个String对象。这显然是错的。如果我们忽略警告并尝试执行这段代码,它将在我们试图使用错误类型的地方上失败。
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return(String) ys.iterator().next(); // run time error
}
当我们从这个List中抽取一个元素,并试图将它当作String对象而把它转换成String时,我们将得到一个ClassCastException的异常。完全相同的情况也发生在了loophole方法的泛型版中。
这种情况的原因就是泛型是由Java编译器作为一种叫做"擦除(Erasure)"的最前到后的机制实现的。你(几乎)可以把它想像为一种"源代码对源代码"(source-to-source)的翻译,这就是为何loophole的泛型版被转换成了非泛型版了。
结果,Java虚拟机的类型安全和完整性再也不处于危险中了,甚至在遇到到未检查的警告时也一样。
基本地,Erasure去除(或者说"擦除")了所有的泛型信息。所有的在角括号中的类型信息都被抛弃了,所以,如像List<String> 这样的参数化类型被转化成了List。所有保持对类型变量使用的地方都被类型变量的高层限度类型(一般就是Object)替换了。并且,无论何时产生的结果都不是类型正确的,一个向适当的类型的强制类型转换被插入了其中。
对Erasure的全部细节的描述超出了本教程的范畴,但我们给出的简单描述离真实情况并不太远。了解一些这方面的知识是有益的,特别是如果你想做一些更加老练的泛型应用,如把已有的API转换到使用泛型时(详见"使用泛型转化遗留代码"),或者只是想理解为什么它们会是这种情况。
在遗留代码中使用泛型代码
现在让我们思考一个颠倒的例子。想像Foolibar.com选择泛型去转化了它们的API,但他们的一些客户端程序还没有转化。所以这些代码看起来像:
package com.Fooblibar.widgets;
public interface Part {
...
}
public 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<Part> parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
Collection<Part> getParts(); // Returns 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和集合框架类库,这两者都在使用泛型。客户端中对泛型类型的使用使得它们成为了原生(Raw Type)类型。
代码行1产生了一个未检查的警告,因为一个原生Collection被传入了一个期望是Collection<Part>出现的地方,而且编译器无法保证这个原生Collection真的就是Part对象的Collection。
作为一种可选的方法,你可以将这些代码作为Java 1.4的源代码进行编译,这就能保证不会出现警告。但这样的话,你将不能使用到JDK 5.0中任何新的语言特性。
--------------------------------------------------------------------------
注意,"Fooblibar.com"是一个纯属虚构的公司,目的仅仅只是为了本文中的例子。任何公司或机构、任何健在或已故的个人与此有关的话,纯属巧合。
译者:看来老外做事情十分谨慎,对于这种"小问题"我们又怎么会如此郑重其事的发表一个声明呢。
7 良好的打印
一个泛型类被它的所有应用共享
下面的代码片断是打印出什么呢?
List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
你可能会被引诱得说是false,但你错了。打印的是true,因为一个泛型类的所有实际拥有相同的运行时类,而不管它们具体的类型参数。
确实,对一个类的泛型所做的事实就是这个泛型类对它所有可能的类型参数都有相同的行为;相同的这个类可以被视为它有很多不同的类型。
同样的结果,泛型类中的静态变量和方法也被该类的所有实例共享。这就是为什么在一个静态方法或初始化器中、在一个静态变量的声明或初始化器中引用类型变量是非法的。
Cast和Instanceof
一个泛型类被它的所有实例共享的另一个隐含意义就是,如果某个实例是这个泛型类的一种特定类型的实例,那么通常情况下请求这个类的实例是无意义的:
Collection cs = new ArrayList<String>();
if (cs instanceof Collection<String>) { ...} // Illegal.
类似地,如下面这个强制类型转换
Collection<String> cstr = (Collection<String>) cs; // Unchecked warning,
会报一个未检查的警告,因为这不应该是运行时系统将要为你检查的事情。
对类型变量也是如此
<T> T badCast(T t, Object o) {return (T) o; // Unchecked warning.
}
类型变量在运行时并不存在。这就意味着在时间和空间上,它们都不可能避免地无法产生作用。不幸的是,这也意味着你不能可靠地在强制类型转换中使用它们。
数组
一个数组对象中元素的类型不会是一个类型变量或参数化的类型,除非它是一个(非受限的)通配符类型。你可以声明数组类型的元素类型是一个类型变量或参数化的类型,但数组对象本身不行。
这很烦人,但却是真的。该约束对避免如下例子中的情况是有必要的:
List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error: ClassCastException.
如果允许有参数化类型的数组,上面的例子将会通过编译且不报任何未检查的警告,然而会在运行时失败。我们已经知道设计泛型的主要目的就是为了类型安全。特别地说,Java语言被设计为,如果你的整个程序使用javac -source 1.5进行编译时没有报任何未检查的警告,那么这个程序就是类型安全的。
然而,你仍然可以使用通配符数组。这儿有上面代码的两个变种。第一个变种放弃使用参数化类型的数组对象和参数化类型元素。这样我们为了在数组外得到String对象不得不在显示地使用强制类型转换。
List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
String s = (String) lsa[1].get(0); // Run time error, but cast is explicit.
在第二个变种中,我们限制了数组对象的创建,这个数组的元素的类型被参数化了,但仍然要将一个参数化的元素类型用于这个数组。这是合法的,但产生一个未检查的警告。确实,这段代码是不安全的,甚至会导致一个错误。
List<String>[] lsa = new List<?>[10]; // Unchecked warning. This is unsafe!
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Correct.
String s = lsa[1].get(0); // Run time error, but we were warned.
译者:根据我的测试(JDK 1.5.0_11),"List<String>[] lsa = new List<?>[10]"这一句无法通过编译,理由也很直观"类型不匹配,不能将List<?>[]转化为List< String>[]"。
类似地,试图创建一个元素类型是类型变量的数组对象会导致一个运行时错误:
<T> T[] makeArray(T t) {
return new T[100]; // Error.
}
因为类型变量在运行时并不存在,这就没有办法确定数组的实际类型。
围绕着这些限制的工作方法是使用了将类字面量当作运行时类型标记的机制,该机制将在下一节"类字面量作为运行时标记"中进行叙述。
8 类字面量作为运行时标记
JDK 5.0的变量之一就是java.lang.Class也被泛型化了。这是一个不容器类而在其它地方使用泛型机制的有趣例子。
既然Class类有一个类型参数T,你可能会问,这个T代表什么?它代表这个Class对象表示的类型。
例如,String.class的类型是Class<String>,而Serializable.class的类型就是Class<Serializable>。这种机制用于提高在你的反射程序中的类型安全性。
特别地,由于Class类中的方法netInstance现在是返回一个T,这样当你在使用反射机制创建对象时能够得到更加精确的类型。
例如,假设你需要一个执行数据库查询的工具方法,给入的是SQL字符串,返回的是数据库中匹配该查询语言的对象的集合。
一种方法就是显示地传入一个工厂对象中,所写的代码就像:
interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc */
for (/* Iterate over jdbc results. */) {
T item = factory.make();
/* Use reflection and set all of item's fields from sql results. */
result.add(item);
}
return result;
}
你可以像下面那么样去调用
select(new Factory<EmpInfo>(){ public EmpInfo make() {
return new EmpInfo();
}}
, "selection string");
你也可以声明一个EmpInfoFactory类去支持Factory接口
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() { return new EmpInfo();}
}
然后像下面那样去调用它
select(getMyEmpInfoFactory(), "selection string");
这个解决方案最终还需要:
* 在调用点使用冗长的匿名工厂类,
* 或者,为每个被使用的类型声明一个工厂类,并将这个工厂类的实例传递到调用点,但这种方法有点不自然。
可以很自然地将类字面量用作工厂对象,这个工厂稍后可被反射机制使用。现在这个程序(不用泛型)可以写为:
Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/* Run sql query using jdbc. */
for (/* Iterate over jdbc results. */ ) {
Object item = c.newInstance();
/* Use reflection and set all of item's fields from sql results. */
result.add(item);
}
return result;
}
可是,这不能给我们一个所期望的精确类型的集合。既然Class是泛型的,我们可以使用下面的代替写法:
Collection<EmpInfo> emps =
sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) {
Collection<T> result = new ArrayList<T>();
/* Run sql query using jdbc. */
for (/* Iterate over jdbc results. */ ) {
T item = c.newInstance();
/* Use reflection and set all of item's fields from sql results. */
result.add(item);
}
return result;
}
上面的程序以一种类型安全的方法给了我们精确类型的集合。
将类字面量作为运行时标记的技术被认为十分狡猾。例如,为了操作Annotation,这种技术在新API中被扩展使用了。
9 通配符的更多趣味
在本节,我们将考虑一些更高级的通配符用法。我们已经看了几个受限的通配符用于读取数据结构时例子。现在反过来想想一个只可写的数据结构。接口Sink是这种类型的一个简单的例子:
interface Sink<T> {
flush(T t);
}
我们可以想像将它作为一个范例用于下面的代码。方法writeAll被设计为刷新集合coll中的所有元素到Sink的实例snk中,并返回最后一个被刷新的元素。
public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
T last;
for (T t : coll) {
last = t;
snk.flush(last);
}
return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // Illegal call.
就已经写出来的,对writeAll方法的调用是非法的,由于无法推断出有效的类型实参;String或Object都不是T的合适类型,因为Collection的元素和Sink必须是相同的类型。
我们可以通过修改writeAll的方法签名来修正这个错误,如下所示,使用了通配符:
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
String str = writeAll(cs, s); // Call is OK, but wrong return type.
该调用是合法的,但赋值是错的,是由于返回类型被推断成了Object,因为T匹配s的类型,但s的类型是Object。
该解决方案使用了一种我们尚未见过的受限通配符形式:有一个较低限度的通配符。语法"? super T"表示未知类型是T的超类型(或者是T本身;记住,超类型关系是弹性的)。
public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
...
}
String str = writeAll(cs, s); // Yes!
使用了这种语法,方法的调用就是合法的,并且被推断的类型正如所愿是String。
现在让我们转向更为实际的例子。java.util.TreeSet<E>表示了一个排序了的以类型为E的对象作为元素的树。构造一个 TreeSet对象的方法之一是传递一个Comparator对象给这个构造器。该Comparator对象将被用于根据期望的规则对TreeSet中的元素进行排序。
TreeSet(Comparator<E> c)
Comparator接口是必须的:
interface Comparator<T> {
int compare(T fst, T snd);
}
假设我们想创建一个TreeSet<String>对象,并传入一个合适的比较器对象。我们就需要一个能比较String的 Comparator对象,一个Comparator<String>就可以做到,但一个Comparator<Object> 对象也能做到。然而,我们不能调用上面Comparator<Object>所提供的构造器。
TreeSet(Comparator<? super E> c)
上述代码允许适用的比较器被使用。
作为最后一个低位受限通配符的例子,让我们看看Collections.max方法,该方法返回一个集合中的极大元素。为了让max文件能够工作,集合中所有的传入该集合的元素都必须实现了Comparable接口。此外,它们相互之间必须是可被比较的。
在第一次尝试创建这个方法后有如下结果:
public static <T extends Comparable<T>>
T max(Collection<T> coll)
即,这个方法有一个某类型T的集合对象,T的实例之间可以进行比较,该方法并返回一个该类型的元素。然而,这个程序实现起来太受限制了。看看是为什么,考虑一个对象,它能与任意对象进行比较:
class Foo implements Comparable<Object> {
...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // Should work.
Collection cf中的每个元素都能与该集合中的其它元素进行比较,因为每个这样的元素都是一个Foo的实例,而Foo的实例能够与任意对象进行比较,则与另一个Foo 对象比较那就更没问题了。然而,使用前面的方法签名,我们可以发现上面对方法max的调用会被拒绝。被推断出的类型必须是Foo,但Foo并没有实现 Comparable<Foo>。
没有必要精确地要求T与它自己的实例进行比较。所有被要求的是T的实例能够与它的某个超类型的实例进行比较。这就让我们有了如下代码:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
注意到Collections.max真实的方法签名更难以理解。我们将在下一节"将遗留代码转化到使用泛型"中再讲述它。这个适用于几乎任何一个 Comprarable应用的理论是打算能用于任意的类型:你总是想使用Comprarable<? super T>。
一般地,如果你的API只是将类型参数T作为类型变量使用,那就应该利于低位受限通配符(? super T)。相反地,如果这个API只需返回T,你就要使用高位受限通配符(? extends T)以给这个API的客户端程序更大的灵活性。
通配符捕获
到目前为此,下面的程序应该更清晰些:
Set<?> unknownSet = new HashSet<String>();
...
/** Add an element t to a Set s. */
public static <T> void addToSet(Set<T> s, T t) {
...
}
但下面的调用是非法的。
addToSet(unknownSet, "abc"); // Illegal.
传入该方法的一个精确的Set是一个String的Set这没有影响;问题在于作为实参传入表达式的是一个未知类型的Set,这并不能保证它一定就是String或其它任何特定类型的Set。
现在考虑下面的代码:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // This works! Why?
看起来它应该不被允许;然而,看看这个特殊的调用,它确实是安全的而可以允许这么做。毕竟,unmodifiableSet方法可用于任何类型的Set,而不管这个Set中的元素的类型。
因为这种情况发生地相对比较频繁,所以有一个特殊的规则允许这些在一个非常特殊的环境中的代码是合法的,在这个环境中这些代码被证明是安全的。这个名为"通配符捕获"的规则允许编译器将通配符的未知类型作为类型实参推断到泛型方法中。
10 将遗留代码转化为使用泛型
早先,我们展示了新、老代码之间如何交互。现在是时候看看"泛型化"老代码这个困难的问题了。
如果你决定将老代码转换成使用泛型,你需要仔细考虑如何去修改你的API。
你需要确定泛型化的API不会造成过度的限制;它必须能继续地支持API原先的功能。再次考虑一些来自于java.util.Collection中的例子。没有使用泛型的API看起来像:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
一种自然的泛型化尝试可能像下面那样:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
肯定是类型安全的了,但它并没有实现该API之前的功能。containsAll方法用于任何引入的集合对象,如果引入的集合真地仅包含E的实例时,该方法才会成功。但是:
* 引入集合的静态类型可能有所不同,或许是因为调用者不知道传入的集合对象的准确类型,或者可能是因为它是一个Collection<S>,而S是E的子类型。
* 能够合法地使用一个不同的类型的集合调用containsAll方法则最为理想了。这种方法应该能工作,并将返回false。
在这个例子中的addAll方法,我们应该能够加入由任何由E的子类型的实例组成的集合对象。我们在"泛型方法"这一节中已经看过了如何正确地处理此类情况。
你也需要保证修改后的API要保持与老的客户端程序的二进制兼容性。这就暗示着"擦除"后的API必须与以前的非泛型化API相同。在大部分例子中,这自然会引用争吵,但也有一些精妙的例子。我们将测试我们已经遇到过的最精妙例子中的一个,即Collections.max()方法。根据我们在"通配符的更多乐趣"一节所看到的,一个模糊的max方法签名是:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
除了擦除后的签名之外,这些都很好:
public static Comparable max(Collection coll)
这与max之前的方法签名不同:
public static Object max(Collection coll)
当然可以这样指定max方法的签名,但这没有什么用。所有老的调用Collections.max方法的二进制class文件都依赖于返回类型为Object的方法的签名。
通过显示地在限度中为形式类型参数T指定一个超类,我们能够强制这个擦除产生不同的结果。
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
这是一个单个类型参数有多个限度的例子,使用语法"T1 & T2 ... & Tn"。有多个限度的类型变量是被认为是限度中所有类型的一个子类型。当使用多限度时,限度中第一个被提及的类型将作为该类型变量被擦除后的类型。
最后,我们应该回想到max方法只需从输入的Collection中进行读取操作,所以这适合于T的任何子类型的集合。
这就把我们带入到JDK中该方法的真实签名中:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在实践中产生如此晦涩的应用是十分罕见的,但是当转换现有API时,专家型的类库设计者们应该要准备着去进行非常细致地地思考。
另一个问题需要密切关注的就是"协变返回",即在一个子类型中精炼了返回类型。你不需要在老的API中使用这个特性。为了找到原因,让我们看一个例子。
假设你原先的API是如下形式:
public class Foo {
public Foo create() {
...
} // Factory. Should create an instance of whatever class it is declared in.
}
public class Bar extends Foo {
public Foo create() {
...
} // Actually creates a Bar.
}
为了利用"协变返回",你将它修改为:
public class Foo {
public Foo create() {
...
} // Factory. Should create an instance of whatever class it is declared in.
}
public class Bar extends Foo {
public Bar create() {
...
} // Actually creates a Bar.
}
现在假设有一个像下面那样写的你代码的第三方客户端程序:
public class Baz extends Bar {
public Foo create() {
...
} // Actually creates a Baz.
}
Java 虚拟机不直接支持有着不同返回类型的方法的覆盖,该特性由编译器支持。因此,除非Baz类被重新编译,否则它不能正常地覆盖Bar的create方法。另外,Baz将不得不被修改,因为这些代码将如前面所写的那样被拒绝--Baz中的create方法返回类型并不是Bar中create方法返回类型的子类型。
译者:根据我的测试(JDK 1.5.0_11),Baz类中的create方法无法通过编译,理由就是Baz.create方法与Bar.create方法的返回不兼容,返回类型须是Bar,而不是Foo。
致谢
Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen, Peter von der Ahe和Philip Wadler为该教程提供了材料。
感谢David Biesack, Bruce Chapman, David Flanagan, Neal Gafter, Orjan Petersson, Scott Seligman, Yoshiki Shibata和Kresten Krab Thorup为该教程的早期版本所提出的富有价值的反馈。向我忘记列出来的每个人道歉。