从今天开始学习Java的泛型(Generics),它能够帮助程序员书写更加可靠的程序和软件。 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类的时候能够使用泛型,那么这个错误在编译的时候就会被发现。 | |
原创java连载--泛型(2) |
我们可以通过将"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个类型。 |
原创java连载--泛型(3) |
泛型方法和构造器 如果在申明方法或者构造器的时候使用类型参数的话,就可以定义泛型方法和泛型构造器。这和定义一个普通的泛型基本上无二样,除了类型参数的作用范围只是在定义它的方法或者构造器之中。 /** * 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 通过输入不同的参数,输出也会跟着相应的变化。 | |
原创java连载--泛型(4) |
受限的类型参数(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 … > | |
原创java连载--泛型(5) |
泛型的子类型 只要两种类型能够相符,我们可以把一种类型的对象赋给另外一种类型的对象。例如,可以把一个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>的子类型。 |
原创java连载--泛型(6) |
通配符 在泛型中,我们可以用一个通配符”?”来代替一个未知的类型。例如,使用下面的代码为某种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); |
原创java连载--泛型(7) |
类型擦除(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