转自:http://blog.21ic.com/user1/1202/archives/2008/53356.html
1. 介绍
通 常,缺陷严重影响着大型程序和软件的使用。通过周密的设计、编码和测试,或许可以减少一些缺陷,但是缺陷对程序来讲简直就是无孔不入,特别是在要引入一些 新的特性或者程序越来越大越来越复杂的时候。值得我们高兴的是有些缺陷能很容易被发现,这给我的工作带来了极大的方便。例如编译时的缺陷能够立刻告诉我们 某个地方有错误,我们也可以通过编译输出的错误信息判断和找出错误所在,并且修改它。运行时的缺陷就没有那么好对付了,因为它们隐藏的很深,喜欢和我们玩 捉秘藏,往往很难被发现,有时即使在某些时候我们发现了它们,要找到产生它们原因的道路还很曲折。泛型 (Generics) 能够帮助我们在编译程序的时候就发现更多的缺陷。
下面代码定义了一个 Box 类,有两个方法 add() 和 get() 。
public class Box {
private Object object;
public void add(Object object) {
this.object = object;
}
public Object get() {
return object;
}
}
因为它的方法的参数是 Object ,所以我们可以随意传递任何参数到 add() 方法中,即使我们要想传递的不是原始类型也没有关系。然而事实上,我们应该严格限制传递的参数类型,但是目前我们只能做的就是在文档中加以说明或者在代码中添加相应的注释,编译器却对参数类型所作限制是不得而知的。
public class BoxDemo {
public static void main(String[] args) {
// ONLY place Integer objects into this box!
Box integerBox = new Box();
// Imagine this is one part of a large application
// modified by one programmer.
integerBox.add("10"); // note how the type is now String
// ... and this is another, perhaps written
// by a different programmer
Integer someInteger = (Integer)integerBox.get();
System.out.println(someInteger);
}
}
在 BoxDemo 中,没有将字符串 ”10” 转换成整数对象,很明显是个缺陷。但是在编译的时候并不能发现这个缺陷,从而造成在运行的时候出现错误。如果在定义 Box 类的时候能够使用泛型,那么这个错误在编译的时候就会被发现。
我们可以通过将 "public class Box" 修改为 "public class Box<T>" 而定义一个泛型,在这个定义中,使用了一个类型变量 (type variable) T ,而且 T 能够在 Box 类之内的任何地方被使用。这中定义的方法其实并不复杂,并且在接口 (interface) 中也被使用。实际上, T 可以看作是一种特殊的数据类型,它的值就是我们要传递给它的参数,参数的类型可以是类,也可以是接口,或者其他类型的变量,但是却不能是原始类型 (primitive) 的数据。
/**
* Generic version of the Box class.
*/
public class Box<T> {
private T t; // T stands for "Type"
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
}
如上所示,我们用 T 代替了其中所有的 Object 。为了在我们的代码中能够使用泛型类,我们必须要用具体的类型比如 Integer 来代替 T ,这也叫做泛型调用 (generic type invocation) 。
实际上,泛型调用和普通的方法调用非常相似,所不同的是方法调用时传递的是参数,而泛型调用是传递的是一种具体的类型参数,比如 Integer 。泛型调用也叫做参数化类型 (parametersized type) 。
为了实例化使用泛型技术定义的类,和通常一样要使用 new 关键字,如下所示:
integerBox = new Box<Integer>();
或者如果写的更加全面的话可以用下面的语句:
Box<Integer> integerBox = new Box<Integer>();
一旦 integerBox 被实例话了,我们就可以使用它的 get 方法而无需进行参数的转换。而且如果试图想加一个类型不符的参数到 box ,编译器就会报错。
我们一定要记住,类型变量并不是真正的数据类型本身,上面的例子中,在文件中是找不到 T.java 或者 T.class 的,而且 T 也不是类名 Box 的一部分。实际上在编译的时候,所有和泛型有关的信息都会被去掉,从而最后只有 Box.class 文件,这会在后面进一步讨论。
另外还要注意的是泛型可以多个类型参数,但是每个类型参数在所定义的类或者接口中不能重复。例如 Box<T, T> 则是有问题的,而 Box<T, U> 则是可以使用的。
按照惯例,类型参数一般使用单个大写的字母表示。这样就可以普通变量的命名形成了明显的对比。如果不按照此惯例,就很难区分类型参数名和普通的类名或者接口名。
通常使用的类型参数如下:
E - 元素 (Element) ;
K - 关键字 (Key) ;
N - 数 (Number) ;
T - 类型 (Type) ;
V - 值 (Value) ;
S , U , V 等 - 第 2 个,第 3 个,第 4 个类型。
泛型方法和构造器
如果在申明方法或者构造器的时候使用类型参数的话,就可以定义泛型方法和泛型构造器。这和定义一个普通的泛型基本上无二样,除了类型参数的作用范围只是在定义它的方法或者构造器之中。
/**
* This version introduces a generic method.
*/
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.add(new Integer(10));
integerBox.inspect("some text");
}
}
在上面的例子中,我们定义了一个泛型方法 inspect ,而该泛型方法定义了一个类型参数 U 。这个方法接收输入的参数并将其类型输出,为了比较,它也将 T 的类型输出。这个类中还有一个 main 方法使得它可以直接运行,而且输出如下:
T: java.lang.Integer
U:java.lang.String
通过输入不同的参数,输出也会跟着相应的变化。
受限的类型参数 (Bounded Type Parameters)
有时候,我们要限制传递给类型参数的具体参数。例如,对数进行操作的方法就只能接受 Number 或者其子类的对象作为改方法的参数,而不能接受其他类型的参数。这也就是要对参数类型进行限制的原因。
在申明一个类型参数的时候,如果在类型参数名后跟着 extends 关键字,而 extends 关键字后面又跟着类型参数的上限 (upper bound) ,例如这个上限可以是个数类 Number ,那么这个被申明的类型参数就是一个受限的参数类型。需要注意的是,这里的 extends 关键字可以是普通意义上类“继承”的意思,也可以是接口上“实现”的意思。
/**
* This version introduces a bounded type parameter.
*/
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number > void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.add(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
如上代码所示, U 就是一个受限的类型参数,只能向其传递 Number 类或者 Number 子类的参数。如果对上面的代码进行编译的时候,就会报错,原因就是调用 inspect 方法的时候向其传递了一个 String 的参数 "some text" 。
在定义受限类型参数的时候,如果还想要实现接口的话,就可以将要实现的接口使用 & 字符连接在类型参数上限 (upper bound) 的后面,如下所示:
<U extends Number & MyInterface>
要想实现多个接口的话,就用 & 依次将要实现的接口跟在后面就可以了,如下:
<U extends Number & MyInterface1 & MyInterface2 … >
泛型的子类型
只要两种类型能够相符,我们可以把一种类型的对象赋给另外一种类型的对象。例如,可以把一个 Integer 赋给一个 Object ,因为 Object 是 Integer 的父类之一。
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
在面向对象的编程中, Integer 和 Object 之间的这种关系叫做 ”is a” ,也就是说在本质上是相同的, Integer 是一种 Object ,所以才允许上面的这种赋值。由于 Integer 也是 Number 的一种,所以下面的赋值也是有效的:
public void someMethod(Number n){
// method body omitted
}
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
对于泛型来讲,上面的讨论的这种关系也是适用的。在一个泛型调用中,我们可以传递 Number 作为它的参数,但是在后续调用 add 方法的时候,我们使用和 Number 相符的类型作为其参数:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
我们现在考虑下面的方法:
public void boxTest(Box<Number> n){
// method body omitted
}
上面这个方法能够接受的参数是什么呢?实际上,我们可以看出它的参数是 Box<Number> 。那么我们能不能传递 Box<Integer> 或者 Box<Double> 作为它的参数呢?不能,原因就是 Box<Integer> 和 Box<Double> 不是 Box<Number> 的子类型。
通配符
在泛型中,我们可以用一个通配符 ” ? ” 来代替一个未知的类型。例如,使用下面的代码为某种 animal 指定一个 cage :
Cage<? extends Animal> someCage = ...;
"? extends Animal" 表示一种未知的类型,它可能是 animal 的一种子类型,也可能是 animal 自己本身,总的来讲就是某种 animal 。上面的例子是一种受限通配符,它的上限就是 Animal 。如果需要装下某种 animal 的 cage ,那么就可以被用作是 lion cage 或者 butterfly cage 。
如果使用 super 而不是 extends 则就可以为未知类型指定一个下限 (lower bound) 。例如, Cage<? super Animal> 表示的也是一种未知类型,其可能是 animal 的一种超类型 (supertype) ,也可能是 animal 自己本身。当然,如果我们用 <?> 来定义一个未知类型,那么这样的未知类型是不受限的。一个不受限的未知类型实质上就是 <? extends Object> 。
虽然 Cage<Lion> 和 Cage<Butterfly> 不是 Cage<Animal> 的子类型,但是却是 Cage<? extends Animal> 的子类型。上面已经定义了 someCage ,那么就可以进行如下赋值:
Cage<Lion> lionCage = ...;
Cage<Butterfly> butterflyCage = ...;
someCage = lionCage; // OK
someCage = butterflyCage; // OK
但是我们还是不能把 butterflies 和 lions 直接 add 到 someCage :
interface Lion extends Animal {}
Lion king = ...;
interface Butterfly extends Animal {}
Butterfly monarch = ...;
someCage.add(king); // compiler-time error
someCage.add(monarch); // compiler-time error
如果 someCage 是一个 butterfly cage ,那么它装入 butterfly 是没有问题的,但是却装不了 lion 。当然,如果 someCage 是一个 lion cage ,那么它装入 lion 是没有问题的,却装不了 butterfly 。也就是我们不能向 someCage 种装入任何 anmial ,那么是不是 someCage 就没有任何用了呢?其实不然,例如下面的代码就用到了 someCage :
void feedAnimals(Cage<? extends Animal> someCage) {
for (Animal a : someCage)
a.feedMe();
}
这样一来,我们就可以把每种 animal 装入到对应独立的 cage 中,然后依次调用这个方法,如下:
feedAnimals(lionCage);
feedAnimals(butterflyCage);
或者把所有的 animal cage 组合起来,然后可以用下面的代码进行代替:
feedAnimals(animalCage);
类型擦除 (Type Erasure)
当我们实例化一个泛型的时候,编译器使用一种叫做类型擦除 (type erasure) 的技术。在类型擦除的过程中,编译器会去除掉 类与接口中所有和类型参数有关的信息。类型擦除使得用泛型的 java 应用程序能够和该泛型创建之前就存在的 java 库和应用程序相兼容。
例如 Box<String> 在编译的时候产生一个叫做原型 (raw type) 的类型 Box ,而所谓原型就是没有任何参数的泛型类名或者接口名。这也就是说,在运行的时候我们不知道一个泛型类究竟是什么类型的对象。如下的代码在编译的时候就会报错:
public class MyClass<E> {
public static void myMethod(Object item) {
if (item instanceof E ) { //Compiler error
...
}
E item2 = new E() ; //Compiler error
E[] iArray = new E[10] ; //Compiler error
E obj = (E)new Object() ; //Unchecked cast warning
}
}
上面黑体代码之所以在编译的时候会报错是因为编译器去除了所有和参数 ( 由类型参数 E 代表 ) 相关的信息。
有了类型擦除技术之后,就可以让新的代码和遗留的代码共存。但是无论如何,使用原型是一种不好的编程习惯,应该避免在程序中使用。当把遗留代码和泛型代码混合在一起的时候,我们可能会碰到类似于下面的告警:
Note: WarningDemo.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
例如,我们用一个旧的 API ,但参数却用的是一个原型参数,如下的代码所示:
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
我们用 -Xlint : unchecked 重新编译就会显示出如下附加的信息:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning