IBM®
跳转到主要内容
    中国 [选择]    使用条款
 
 
Select a scope:Search for:    
    首页    产品    服务与解决方案     支持与下载    个性化服务    
跳转到主要内容

developerWorks 中国  >  Java technology  >

编译时使用 Generic Java 捕获更多的错误

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Keith Turner (rkturner@us.ibm.com), 软件工程师, IBM

2001 年 3 月 12 日

当前实现泛型类的方法,例如 VectorHashtable ,要求很多难看的强制类型转换,这也许会导致实时运行错误。参数类型提供了一种实现泛型类的简明方法,它可以减轻强制类型转换的需要,允许更多的错误在编译时被捕获。GenericJava(泛型 Java)是在 Java语言里增加参数类型的一项提议,并可以支持以前编写的代码和先前存在的Java 虚拟机。

Java 编程语言的简明和可表达性已赢得了理论家和开发人员的芳心。附加功能也许会增强 Java 语言,但是达成一个它应包括什么和如何实现它的共识却很困难。例如,曾经有过关于是否包括操作符重载的激烈讨论。另一方面,公众普遍认同,参数类型可以加入到语言规范中。争论存在于参数类型应当如何被加入。一项受到普遍认可的提议是调用 Generic Java(GJ)。本文将介绍参数类型和讨论 GJ 的一些优缺点。

参数类型简介

参数类型是一个类,它拥有一组与之关联的类型变量。这些类型变量允许程序员在编写一个新类时,保留一些类型不被指定。这种自由使得可以实现一组易在多种类型下使用的函数。清单 1 提供了参数类型的示例,它通过使用在参数列表 <A,B> 里定义的类型变量 AB 来定义 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 的声明。在声明里,类型变量 AB 被分别赋予值 StringCharacter 。因而在类 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> 之间的赋值是不允许的。这非常好理解,因为在类型 StringInteger 之间的赋值是非法的。可是当涉及子类型时,事情就变得有点复杂。在 Java 语言里进行从 String 类型变量到 Object 类型变量的赋值完全是合法的。进行从 Pair1<String, Float> 类型变量到 Pair1<Object, Float> 类型变量的赋值看上去似乎也是合法的,但在 GJ 里它是非法的。为了合法,实际的类型参数必须与赋值的类型相同。





回页首


GJ 的普遍用法

清单 1 里的 Pair1 类是一个泛型类的示例。说它泛型,是因为在不同的类型下,可以方便地使用相同的功能。 VectorHashtableEnumeration 提供了泛型类的三个示例,这些泛型类可在不同的类型下被方便地使用。使用泛型类可以提供一些很有意义的优势。首先,泛型类仅编写一次并且被大量测试,这样使用它们将使程序更稳定。其次不必浪费时间去开发泛型类能够提供的普通功能,允许把更多的时间花在关于某个应用程序的特定问题上。

参数类型的普遍用法是创建泛型类。可是,当前 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 是一个使用增强的 VectorEnumeration 类的简单示例。代码声明了一个仅能容纳 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;
   }
}

上面的输出基本上是与 清单 3Pair2 类相同的。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 中的两个赋值。可是, rawVectorstringVector 的赋值将产生一个警告,因为类安全不能得到保障。程序员能规定 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。




对本文的评价

太差! (1)
需提高 (2)
一般;尚可 (3)
好文章 (4)
真棒!(5)

建议?




回页首


IBM 公司保留在 developerWorks 网站上发表的内容的著作权。未经IBM公司或原始作者的书面明确许可,请勿转载。如果您希望转载,请通过 提交转载请求表单 联系我们的编辑团队。
    关于 IBM 隐私条约 联系 IBM 使用条款