浅谈Java泛型编程
1 引言在JDK
1.5中,几个新的特征被引入Java语言。其中之一就是泛型(generics)。泛型(generics,genericity)又称为“参数类型化
(parameterized
type)”或“模板(templates)”,是和继承(inheritance)不同而互补的一种组件复用机制。继承和泛型的不同之处在于——在一个
系统中,继承层次是垂直方向,从抽象到具体,而泛型是水平方向上的。当运用继承,不同的类型将拥有相同的接口,并获得了多态性;当运用泛型,将拥有许多不
同的类型,并得以相同的算法作用在它们身上。因此,一般说来,当类型与实现方法无关时,使用泛型;否则,用继承。
泛型技术最直接联想到的用途就是建立容器类型。下面是一个没有使用泛型技术的例子: List myIntList = new
LinkedList();// 1 myIntLikst.add(new Integer(0));// 2 Integer x =
(Integer)myIntList.iterator().next();// 3
显然,程序员知道究竟是什么具体类型被放进了myIntList中。但是,第3行的类型转换(cast)是必不可少的。因为编译器仅仅能保证
iterator返回的是Object类型。要想保证将这个值传给一个Integer类型变量是安全的,就必须类型转换。除了使代码显得有些混乱外,类型
转换更带来了运行时错误的可能性。因为程序员难免会犯错误。使用了泛型技术,程序员就可以确切地表达他们的意图,并且把myIntList限制为包含一种
具体类型。下面就是前一个例子采用了泛型的代码段: List<Integer> myIntList = new
LinkedList<Integer>();// 1 myIntLikst.add(new Integer(0));// 2
Integer x = myIntList.iterator().next();// 3
List<Integer>指出了这不是一个随意的List,而是一个Integer的List。我们说List是一个带有类型参数的泛型接
口,在这里就是指Integer。现在,我们在第1行里使用Integer作为类型参数,而不是在第3行里做类型转换。这样,在编译时刻,编译器就能够检
查程序的正确性——无论何时何地,编译器都将保证myIntList的正确使用。相反地,类型转换仅仅告诉我们——在这里,程序员认为这样做是对的。采用
泛型可以增强代码可读性和健壮性(robustness)。
2 定义泛型 public interface List<E> { void add(E x);
Iterator<E> iterator(); } public interface Interator<E> {
E next(); boolean hasNext(); }
这是一段Collection里代码,一个完整的泛型定义。尖括号里的E就是形式类型参数(formal type
parameters)。在泛型定义中,类型参数的用法就像一般具体类型那样。在引言中,我们看到初始化了一个泛型List——List<
Integer>。在这里,类型参数被赋于实际类型参数(actual type
argument)Integer。你可以想象List<Integer>将获得这样的代码: public interface
List { void add(Integer x); Iterator< Integer > iterator();
}
和C++中对模板的处理有很大的不同,这里没有第2份副本。Java采用的是拭去法(erasure)而C++采用的是膨胀法(expansion)。一
个泛型定义只被编译一次,只生成一个文件,就像一般的class和interface一样。形式类型参数可以不止1个,如: class Bar
< E, D> { …… }
3 通配符 3.1 泛型和子类下面的这段代码合法么? List<String> ls = new
ArrayList<String> ();// 1 List<Object> lo = ls;// 2
假设这两行代码是正确的,那么下面的操作: lo.add(new Object());// 3 String str =
ls.get(0);// 4 将导致运行时刻错误。通过别名lo存取ls时,我们可以插入任意类型的对象——ls就不再仅仅持有String了。
Java编译器消除了这种错误发生的可能性。第2行将导致编译时刻错误。一般地说,如果Foo是Bar的子类,G定义为某种泛型,那么G<
Foo>不是G<Bar>的子类。
3.2 通配符如果,我们试图使用泛型的方法编写一个打印Collection内所有元素的函数,要怎么做? void
printCollection (Collection<Objcet> c) { for (Objcet obj : c)
{// jdk 1.5中新增的语法,见5.1 System.out.println(obj); } }
显然这样是不行的,因为通过3.1我们可以知道——Collection<Object>不是任何Collection的父类。那么,所有
Collection的父类是什么?Collection<?>——未知类型的Collection(collection of
unknown),一个元素可以匹配为任意类型的Collection。“?”被称作通配类型。上述的代码,可以改写成这样: void
printCollection(Collection<?> c) { for (Object obj : c) {
System.out.println(obj); } }
现在,我们可以使用任意类型的Collection作为参数了。注意,在printCollection内,用Objcet类型访问c的元素是安全的,因
为任何一种具体类型都是Object的子类。但是这样的操作是错误的: List<?> list = new
ArrayList<String>(); list.add(…);// compile-time error!
因为list被定义为List<?>,“?”指代了一个未知类型。list.add(…)无法保证插入的对象类型就是list实际包含的类
型。唯一的例外就是null——null可以是任意类型的值。但是,通过一个List<?>引用,调用get()函数是可以的——即不会修改
Collection的函数,就像printCollection里那样。尽管不能确定具体的类型,但是都是Object的子类。
3.3 受限通配符现在要创建一个简单的作图程序。我们定义了接口Shape: public abstract class
Shape { public abstract void draw(); } 然后定义了2个子类: public class
Circle extends Shape { ……. public void draw() { … } } public
class Rectangle extends Shape { …… public void draw() {……} }
很自然地,我们也会设计这样一个函数: void drawAll (List<…> shapes) { for (Shape
s : shapes) { s.draw(); } }
尖括号里应该填写什么了?显然,List<Shape>是行不通的,这在3.1里已经说明了。List<?>可以,但是不好,因
为如果这样使用: List<Object> list = new ArrayList<Object>();// 1
list.add(new Object());// 2 drawAll(list);// 3
编译器认为没有问题,但是运行时刻肯定报错。在drawAll里,我们实际需要的是Shape的子类,但是List<?>无法在编译时刻保证
这一点。这里的解决方案是受限通配符(bounded wildcard)。这样做: void drawAll(List<? extends
Shape> shapes) { .. … }
如果,再像前一个例子的第3行那样使用的话,编译器会报错。因为编译器要求shapes的每一个元素的实际类型都是Shape的子类。同使用一般通配符一
样,shapes.add(…)是不允许的,因为,编译器只能保证插入的是Shape的子类对象,而不能肯定与Collection实际包含的类型是匹配
的。
4 泛型函数考虑设计这样一个函数——把一个数组中的对象依次插入一个Collection中。我们首先这样尝试: void
addFromArray(Object[] a, Collection<?> c) { for (Object o : a)
{ c.add(o);// compile-time error! } }
从前面的介绍中,可以明确这样是不行的。当然Collection<Object>同样是错误的。解决这类问题的方法就是使用泛型函数:
static <T> void addFromArray(T[] a, Collection<T> c) {
for (T o : a) { c.add(o); } }
但是必须注意,当我们执行addFromArray时,编译器将根据参数的类型检查是否安全: addFromArray(new
String[10], new ArrayList<String>());// OK! addFromArray(new
String[10], new ArrayList<Object>());// OK! addFromArray(new
Object[10], new ArrayList<String>());// compile-time error!
addFromArray(new String[10], new ArrayList<Integer>());//
compile-time error!
第3,4行的错误是很容易理解的,无论是把一个Object类型对象插入String的List还是把一个String插入Integer的List都是
不安全的。不过,如果这样的代码是没有问题的: <T> void foo(T t1, T t2) {
System.out.println(t1.getClass());
System.out.println(t2.getClass()); } foo(new Object(), new
String());// 显示 class java.lang.Objectclass.lang.String foo(new
Integer(), new String();// 显示 class java.lang.Integerclass.lang.String
foo(new Object[10], new ArrayList<String>()); // 显示 class
[Ljava.lang.Object;class.util.ArrayList foo(new String[10], new
ArrayList<Integer>()); // 显示 class
[Ljava.lang.String;class.util.ArrayList
至于每一种调用T究竟是匹配了哪种类型。注意:这不是C++。经过编译,foo只生成一段代码,T就是Object。编译器只是在恰当的地方做了恰当的类
型转换。
4.1 泛型函数和通配符的选择什么时候应当使用泛型函数,什么时候应当使用通配符呢?先看一段来自Collection里的代码:
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); }
在containsAll和addAll中,类型参数T仅仅被使用了一次。函数返回值并不依赖于类型参数。这就告诉我们,类型参数是被用于实现多态的;它
的作用仅仅是允许不同的实际类型在不同的场合下可以被使用。如果是这种情况的话,应当使用通配符。通配符用来实现弹性的子类化——就像这里试图表达的那
样。泛型函数允许类型参数用来表达函数以及它的返回值和一个或多个类型参数之间的依赖性。如果,不存在这样的依赖性的话,泛型函数就不应当被使用。泛型函
数和通配符有时是可以一起使用的,如: class Collections { public static <T> void
copy(List<T> dest, List<? extends T> src) { … } }
注意两个参数之间的类型依赖性。src内包含的对象必须满足is-a
T,只有这样才能够被安全的插入dest,因为dest包含的对象是T类型的。当然这样也可以的: public static <T, S
extends T> void copy(List<T> dest, List<S> src) { … }
但是推荐第一种用法。因为T同时对dest和src起作用,而S仅仅作用于src,没有其他的什么依赖于它——这种情况下,用通配符取代S比较好。用通配
符更加清晰、明了。
5 其他 5.1 增强型for(Enhanced for,foreach)增强型for也是JDK
1.5新引入的Java语法。与传统的for相比,具有代码清晰,安全的优点。 List<Integer> list= new
ArrayList<Integer>(); int result = 0; for (Integer i : list) {
result += i.intValue(); } 相当于: for (Iterator iter = list.iterator();
iter.hasNext();) { result += ((Integer)i.next()).intValue(); }
同样也可以作用于数组: Integer[] ia = new Integer[10]; int result = 0; for
(Integer i : ia) { result += i.intValue(); }
5.2 通配符和重载,泛型函数和重载 void foo(List<String> ls) {
System.out.print(“foo(List<String> ls)”); } void
foo(List<Object> lo) {
System.out.print(“foo(List<Object> lo)”); } void
foo(List<?> l) { System.out.print(“foo(List<?> l)”); }
foo(new ArrayList<String>()); foo(new
ArrayList<Object>()); foo(new ArrayList<Integer>());
编译并运行这段代码,我们能看到什么?……编译错误——“hava the same
erasure”。注意,Java针对泛型采取的是拭去法,不论是List<String>,List<Object>还是
List<?>,编译生成的都是同一段代码,而且这段代码和非泛型的List在本质上是一样的。可以这样认为,Java编译器对泛型的处理只
是替我们在适当的地方加上了类型转换而已。所以以上3个foo函数不构成重载。类似的代码在C++中是可行的,因为C++采用的是膨胀法。针对不同的具体
类型,生成不同的副本,List<String>和List<Object>是2个不同类型(STL里没有Object类型,
String应为std::string),因此foo满足重载的条件。这种用法称为“显式特化”(explicit specialization
definition)。
再看一下下面这段代码: void foo(String s) {
System.out.println(“foo(String s)”); } void <T> foo(T t) {
System.out.println(“foo(T t)”); } foo(“Test”); foo(new Integer(1));
编译并运行这段代码,我们能看到什么?……编译错误?不是! foo(String s) foo(T t)。正是预期的输出。现在,修改一下:
void foo(Object o) { System.out.println(“foo(Object o)”); } void
<T> foo(T t) { System.out.println(“foo(T t)”); }
不用尝试任何例子,因为这已经无法通过编译了: name clash: foo(java.lang.Object o) and
<T>foo<T> hava the same erasure 拭去法是这样处理泛型的:
l一个参数化类型擦拭后应该除去参数(List<T> è List) l一个未受限的类型参数擦拭后成为Object
l一个受限的类型参数擦拭后成为bound的类型
但是需要注意以下的代码: class Foo<E> { public void
test1(List<E> list) { … };// List<E>擦拭后èList public
<T> void test2(T t) { … }// T擦拭后èObject } class Bar<E, F>
extends Foo<F> { public void test1(List<E> list) { … }//
compile-time error public void test2(Object o) { … }// compile-time
error } 注意不是是覆盖(override)……
5.3 数组 List<String>[] list = new
ArrayList<String>[10]; 似乎是正确的……编译时错误! List<String>[] list =
new ArrayList<String>[10];// 1 Object o = list;// 2 Object[] oa =
(Object[])o;// 3 oa[1] = new ArrayList<Integer>();// 4 String s =
list[1].get();// 5
如果第1行是正确的话,那么第5行就会出现运行时错误,因为2—5行的语法都是没有问题的。泛型数组只能这样用: List<?>[]
list = new ArrayList<?>[10];
这解决问题了么?没有。因为错误还是无法避免,除了第5行必须一个显式的类型转换。 List<String>[] list = new
ArrayList<String>[10];// 1 Object o = list;// 2 Object[] oa =
(Object[])o;// 3 oa [1] = new ArrayList<Integer>();// 4 String s
= (String)list[1].get();// 5 explicit cast
5.4 新建参数类型的对象 <T> static void foo(T t) { //….. T tt
= new T();// compile-time error }
又是一个和C++模版的不同之处。Java采取的是拭去法!所以,试图新建一个参数类型对象的话,应当这样: <T> static
void foo (T t, Class<T> klass) {// JDK 1.5中,Class类用泛型改写了 //
….. try { T tt = klass.getInstance(); } catch (…) { } }
参考资料: [1] Generics in the Java Programming Language
http://java.sun.com/j2se/1.5/pdf/generics-tutorial.pdf [2] Forthcoming
Java Programming Language Features
http://java.sun.com/j2se/1.5/pdf/Tiger-lang.pdf [3]
侯捷·Java泛型技术之发展·程序员,2002年第8,9期 [4] 紫云英·漫谈面向对象程序设计方法·程序员,2002年第3期