级别: 初级 Keith Turner (rkturner@us.ibm.com), 软件工程师, IBM
2001 年 3 月 12 日 当前实现泛型类的方法,例如
Vector 和
Hashtable ,要求很多难看的强制类型转换,这也许会导致实时运行错误。参数类型提供了一种实现泛型类的简明方法,它可以减轻强制类型转换的需要,允许更多的错误在编译时被捕获。GenericJava(泛型 Java)是在 Java语言里增加参数类型的一项提议,并可以支持以前编写的代码和先前存在的Java 虚拟机。
Java
编程语言的简明和可表达性已赢得了理论家和开发人员的芳心。附加功能也许会增强
Java
语言,但是达成一个它应包括什么和如何实现它的共识却很困难。例如,曾经有过关于是否包括操作符重载的激烈讨论。另一方面,公众普遍认同,参数类型可以加入到语言规范中。争论存在于参数类型应当如何被加入。一项受到普遍认可的提议是调用
Generic Java(GJ)。本文将介绍参数类型和讨论 GJ 的一些优缺点。
参数类型简介
参数类型是一个类,它拥有一组与之关联的类型变量。这些类型变量允许程序员在编写一个新类时,保留一些类型不被指定。这种自由使得可以实现一组易在多种类型下使用的函数。清单
1 提供了参数类型的示例,它通过使用在参数列表
<A,B> 里定义的类型变量
A 和
B 来定义 pair
的基本功能,而不去理会特殊类型。这些类型变量并不和任何特殊类型相关联,可以出现在一个正常类型出现的任何地方。例如,变量
element1 不和任何特殊类型相关联,随着
A
的变化而变化。
清单 1. 定义参数类型
class Pair1 <A,B> {
private A element1;
private B element2;
public Pair1(A element1, B element2){
this.element1 = element1;
this.element2 = element2;
}
public A getElement1(){
return element1;
}
public B getElement2(){
return element2;
}
} |
当使用参数类型进行声明时,特殊类型和每一个类型变量相关联。清单 2
的第一个语句显示了使用参数类型
Pair1
的声明。在声明里,类型变量
A 和
B
被分别赋予值
String 和
Character 。因而在类
Pair1
的定义里任何地方出现的
A ,您都可以看作是
String 。因此,
testScore.getElement1()
返回的类型、
testScore.element1
变量和构造函数的第一个参数都是
String
类型。同样,
testScore.getElement2()
返回的类型、
testScore.element2
变量和构造函数的第二个参数都是
Character 类型。
清单 2. 使用参数类型
Pair1 <String, Character> testScore;
testScore = new Pair1 <String, Character>("Doe, John", new Character('B'));
String name = testScore.getElement1();
Character grade = testScore.getElement2(); |
GJ
规范不允许用相同的参数类型(使用不同的类型参数)来进行声明的变量之间的赋值。例如,类型变量
Pair1<Integer, Character> 和
Pair1<String, Character>
之间的赋值是不允许的。这非常好理解,因为在类型
String
和
Integer
之间的赋值是非法的。可是当涉及子类型时,事情就变得有点复杂。在 Java
语言里进行从
String 类型变量到
Object
类型变量的赋值完全是合法的。进行从
Pair1<String,
Float> 类型变量到
Pair1<Object, Float>
类型变量的赋值看上去似乎也是合法的,但在 GJ
里它是非法的。为了合法,实际的类型参数必须与赋值的类型相同。
GJ 的普遍用法
清单 1 里的
Pair1
类是一个泛型类的示例。说它泛型,是因为在不同的类型下,可以方便地使用相同的功能。
Vector 、
Hashtable 和
Enumeration
提供了泛型类的三个示例,这些泛型类可在不同的类型下被方便地使用。使用泛型类可以提供一些很有意义的优势。首先,泛型类仅编写一次并且被大量测试,这样使用它们将使程序更稳定。其次不必浪费时间去开发泛型类能够提供的普通功能,允许把更多的时间花在关于某个应用程序的特定问题上。
参数类型的普遍用法是创建泛型类。可是,当前 Java
语言提供的标准泛型类没有使用参数类型来编写。这些类利用了 Java
Object 类,它是 Java
语言里所有类的超类。为了理解这是如何工作的,请考虑清单
3,它是另一个使用
Object 类的泛型 pair 类。
清单 3. 用 Object 类定义一个泛型类
class Pair2{
private Object element1;
private Object element2;
public Pair2(Object element1, Object element2){
this.element1 = element1;
this.element2 = element2;
}
public Object getElement1(){
return element1;
}
public Object getElement2(){
return element2;
}
} |
请注意,
Object 类的使用如何允许
Pair2
类提供的功能可在不同的类型下被方便地使用,正像
Pair1
类那样。为了理解这样的安排,请考虑在清单 4 里的示例,它使用
Pair2 代替了
Pair1 ,完成了和
清单 2
所示的相同的任务。也可以这样观察,因为在清单 3 的代码里使用了
Object 类,所以在清单 4 代码里要求强制类型转换。
清单 4. 使用泛型类
Pair2 testScore2;
testScore2 = new Pair2("Doe, John", new Character('B'));
String name = (String) testScore2.getElement1();
Character grade = (Character) testScore2.getElement2(); |
把清单 4 里的代码与
清单 2
里不要求强制类型转换的代码做比较。为了理解必须做强制类型转换的效果,请考虑下面两个不正确的示例。在清单
5 里的示例产生了一个编译时错误;在清单 6
里的示例导致了一个实时运行错误。
清单 5. 产生一个编译错误的代码段
Pair1<String, Character> testScore;
testScore = new Pair1<String, Character>("Doe, John", new Character('B'));
String name = testScore.getElement2();
Character grade = testScore.getElement1(); |
在清单 5 里,语句:
String name = testScore.getElement2() |
导致了一个编译错误,因为
testScore.getElement2() 的返回类型是 Character。可是,语句:
String name = (String) testScore2.getElement2() |
在清单 6 里没有任何问题,因为
testScore2.getElement2() 的返回类型是
Object 。因此,当试图将
testScore2.getElement2() 返回的
Character
对象作为
String
来进行强制类型转换时,一个强制类型转换的异常发生了。
清单 6. 产生实时运行错误的代码段
Pair2 testScore2;
testScore2 = new Pair2("Doe, John", new Character('B'));
String name = (String) testScore2.getElement2();
Character grade = (Character) testScore2.getElement1(); |
使用
Object 类的问题是,我们没法说明在一个通用对象里我们打算存储什么类型。相反,参数类型允许我们说明在一个通用对象里我们打算存储什么类型。当您清楚地表明您的意图后,编译器能执行您的愿望。这允许编译器捕获更多也许将在实时运行时发生的错误。为了捕获实时运行错误,必须运行一些“讨厌的”代码,而错误是否发生取决于测试用例。因而,在使用参数类型可被容易发觉的错误可能会被带入产品代码中。在编译时捕获更多的错误是一个使用 GJ 的非常好的理由。
使用 GJ
使用 GJ 相当愉快和简单。GJ 的设计人员开发了一种叫做 gjc
的编译器。访问他们的网址可免费获得这个编译器(请参阅
参考资料)。gjc
的优点是,它产生的类文件可以运行在未更改的 Java
虚拟机(JVM)上。它还允许使用参数类型来编写代码,使用 gjc
编译,然后在任何 JVM
上运行(我们将在以后讨论这样是可行的理由)。gjc
另一重大功能是,它能在不要求任何修改的前提下,编译所有先前存在的
Java 代码。可是在讨论 Java
编程时,编译器不是需要考虑的仅仅一部分。
一提到 Java 编程,便让我们想起了整个 Java
环境。这个环境由一些关键部分组成:实时运行环境、编译器和标准类库。从设计模板到开发人员能用的东西,GJ
必须考虑这些所有的组件及其功能。gjc
实现者扩展了一些标准类库,以使用参数类型。这是另一个使用 GJ
的重要理由,因为这些类被频繁地使用。当使用作为 Java
语言的标准一部分的公共泛型类时,您就能利用先前讨论的所有优点。
清单 7 是一个使用增强的
Vector 和
Enumeration 类的简单示例。代码声明了一个仅能容纳
String 对象的
Vector 。假如代码试图将
String 对象以外的东西插入到
Vector
里,编译器将会标记这里是一个错误。把这些同正规的
Vector
类的行为作比较,后者任何类型对象的插入都是合法的,即使程序员有意让
Vector 仅仅容纳
String 对象。
清单 7. 使用增强的实用类
import gj.util.*;
class SimpleExample {
public static void main(String args[]){
Vector<String> v = new Vector<String>();
Enumeration<String> e;
v.addElement("GJ Rocks");
v.addElement("GJ is great");
e = v.elements();
while(e.hasMoreElements()){
System.out.println(e.nextElement());
}
}
} |
GJ 的背景资料
当编译清单 7 的代码时,gjc
产生的代码与在缺少参数类型的情况下被正常编写出的代码类似。假如使用
-s 选项调用 gjc,清单 8 里的代码被产生。使用这个选项对看清 GJ
如何工作非常有用。GJ
产生的代码执行了通常用手编写的强制类型转换。这儿的关键是 GJ
产生的强制类型转换在运行时从不失败。这是可以保证的,因为 GJ
使用了程序员给定的额外类型参数来进行类型检查。假如有错,编译将失败。
清单 8. gjc -s 的输出
import gj.util.*;
class SimpleExample {
SimpleExample() {
super();
}
public static void main(String[] args) {
Vector v = new Vector();
Enumeration e;
v.addElement("GJ Rocks");
v.addElement("GJ is great");
e = v.elements();
while (e.hasMoreElements()) {
System.out.println((String)e.nextElement());
}
}
} |
除了增加强制类型转换以外,gjc
删除了来自于源代码的所有参数类型信息。GJ 开发人员把移去参数信息称作
erasure。
清单 8 的代码也许不是很明显,但当从参数类型中生成类文件时,GJ
使用了
Object 类。如果在清单 9 中观察对
Pair1 类(在
清单 1
中)使用 gjc -s 的输出,这将更加明显。
Object
类的使用连同 erasure 一起,允许 gjc
在被产生的字节码中删除所有的参数类型信息。只要没有了参数类型信息,就允许
gjc 产生的字节码能运行在任何先前存在的 JVM 上。
清单 9. gjc -s 的输出
class Pair1 {
private Object element1;
private Object element2;
public Pair1(Object element1, Object element2) {
super();
this.element1 = element1;
this.element2 = element2;
}
public Object getElement1() {
return element1;
}
public Object getElement2() {
return element2;
}
} |
上面的输出基本上是与
清单 3 的
Pair2 类相同的。GJ 在底层使用
Object
类带来了一些有趣的结果。首先,基本类型不能作为实际类型参数使用。例如,代码
Pair1<int, String>
是非法的。一些程序员认为这是一个 GJ
设计上的缺点。其他人认为它比起以前创建泛型类的方法,没有太多的限制。其次,设计人员选择使用
Object
类意味着每一个泛型类仅需要生成一个类文件。例如,假定 Java
程序声明了
Pair1<String,
Float> 、
Pair1<Integer, Double> 和
Pair1<Vector<Integer>, String>
的类型变量。当代码编译时,仅生成一个
Pair1.class
文件。正如清单 9 所示,GJ
使用强制类型转换让一切都工作正常。对于那些熟悉 C++ 模板的人,C++
对于相同的情形将生成三种中间类。C++
方法导致了更大的目标代码尺寸和速度上的优势。
除了产生可以运行在没有修改的 JVM 上的字节码以外,gjc
可以使用
哑类型(raw types)编译先前存在的 Java
代码。一个哑类型出现在一个声明里,在这个声明中,可以在不给出任何实际类型参数的情况下而使用参数类型。在清单
10 中,
rawVector 变量有
Vector
的哑类型。GJ 允许清单 10 中的两个赋值。可是,
rawVector
到
stringVector
的赋值将产生一个警告,因为类安全不能得到保障。程序员能规定 gjc
仅接受在
stringVector 里的
String
对象。可是,
rawVector 所引用的
Vector
对象也许包含
String
对象以外的其它东西。这有可能,因为哑类型没有类型参数,因此 GJ
无法对它进行类型检查。结果,一个强制类型转换异常可能在实时运行时出现。这样处理哑类型不是
GJ
设计上的缺点,而是使用哑类型不可避免的结果。那就是为什么当从一个哑类型赋值时,GJ
会产生一个警告。当缺少哑类型时,使用以前的代码将不是一项这么容易的任务。
清单 10. 哑类型
Vector rawVector;
Vector<String> stringVector = new Vector<String>();
rawVector = stringVector;
stringVector = rawVector; |
结论
基本上,GJ 建立在 Java
语言创建泛型类的先前方法上。通过这种方法,它可以与以前的 JVM
和类库继续兼容。在保持兼容性的同时,GJ
也帮助开发人员在编译时发现更多的错误。缺点是 GJ
仍旧具有不能使用泛型类里的基本类型的限制。
总而言之,GJ
对于开发人员而言是一个极好的工具:简单、明确和强大。任何要想了解更多的
Java 开发人员可以访问 GJ 网址(请参阅
参考资料)。对于不想使用特别的编译器的人而言,据说参数类型可能以一种与
GJ 兼容的方式,加入到官方的 Java 语言规范中。
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的
英文原文.
-
GJ
主页提供了深层探究 GJ 设计所需的所有参考资料。它包含关于 GJ
算法和 Java 语言的其它功能的信息,例如 GJ 支持的接口和继承。
- 利用参数类型编写的 Java 程序可以使用
gjc来编译。
关于作者  | |  | Keith Turner 最近从 Purdue
大学毕业,并获计算机科学硕士学位。他现在是 IBM
公司的软件工程师,致力于设计一个叫做 TPF
的用于高性能事务处理过程的操作系统。Keith 使用 Java
语言实现了许多工程并且在 Purdue 从事了三个学期的 Java 教学,因此对
Java 语言的熟悉程度达到了一个很高的水平。可通过
rkturner@us.ibm.com 联系
Keith。
|
对本文的评价
|