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

developerWorks 中国  >  Java technology  >

诊断 Java 代码: Impostor Type 错误模式

用标记区别对象类型会导致误贴标签

developerWorks
文档选项

未显示需要 JavaScript 的文档选项


级别: 初级

Eric E. Allen (eallen@cs.rice.edu), 博士研究生

2001 年 7 月 20 日

当使用字段中特殊的标记来区别对象类型时,可能会产生标记对相关数据误贴标签的错误 ― 通称为 Impostor Type 错误模式。在诊断 Java 代码的这一部分中,Eric Allen 对这个错误的症状和起因进行了分析,详细说明了预防错误发生的方法,并讨论了一种吸引人的混合实现方法,这种方法不使用 impostor type,但最后,还是有很多相同的缺点产生。请在 讨论论坛与作者及其他读者分享您对本文的看法。

程序中除了最无关紧要的部分外都要对某些数据类型进行操作。静态类型系统提供了一种方法,它能够确保程序不会对给定类型的数据进行不当的操作。Java 语言的优点之一是严格的区分类型,所以在程序运行前已消除了类型错误。作为开发人员,我们可以使用这个类型系统提供更健壮且没有错误的代码。然而,我们却常常没有让类型系统发挥出最大的潜力。

Impostor Type 错误模式

很多程序可以更多地使用静态类型系统,但它们没有这样做,而是依赖包含区别数据类型标记的特殊字段。

代码快速跟踪

清单 1. 用 impostor type 实现几何形状
一个基本示例,演示了引入这类错误是如何的轻而易举。 清单 2. 构造新的 form
引入了错误。 清单 3. 用实际类型实现 form
这种新颖的方法可以在运行时报错。 清单 4. 一种混合的实现方式不使用 impostor type,但易受性一样。

依靠这些特殊字段区别数据类型,这样的程序放弃了类型系统专门提供给它们的保护措施。当这些标记中的一个对它的数据误贴了标签,就会产生我称之为 Impostor Type的错误。





回页首


症状

impostor type 错误的一种常见症状是很多概念上不同类型的数据都被同样(并且错误)的方式处理。另一常见症状是数据与任何指定的类型都不匹配。

首要规则是,只要当概念上的数据类型和它被程序处理的方法不匹配,就可以怀疑是否发生了这个模式的错误。

为说明引入这种模式的错误是多么的轻而易举,让我们来考虑一个简单的示例。假设我们需要处理各种各样的欧几里得几何学形状,如圆形、正方形等等。这些几何形状没有坐标,但含有一个 scale 变量,所以可以计算它们的面积。


清单 1. 用 imposter type 实现各种几何形状
public class Form {
     String shape;
     double scale;
     public Form(String _shape, double _scale) {
         this.shape = _shape;
         this.scale = _scale;
     }
     public double getArea() {
         if (shape.equals("square")) {
             return scale * scale;
         }
         else if (shape.equals("circle")) {
             return Math.PI * scale * scale;
         }
         else { // shape.equals("triangle"), an equilateral triangle
             return scale * (scale * Math.sqrt(3) / 4);
         }
     }
 }           


尽管您会发现人们经常这么做,但用这种方法实现几何形状还是存在严重缺点。

最显著的缺点之一是这个方法不能真正的扩展。如果要为我们的 form 引入一个新的几何形状(比如,“五边形”),我们必须进入并修改 getArea() 方法的源代码。不过可扩展性是个独立的考虑因素;在本文中,我们把重点放在实现几何形状所造成的错误的易受性上。我会在以后的文章中回到关于可扩展性的问题上来。

如果我们在程序其它部分构造了一个新的 Form 对象,如下所示,请考虑将会发生什么情况:


清单 2. 构造一个新的 form
 Form f = new Form("sqaure", 2);


当然,“square”被拼错了,但是编译器认为,这是完全合法的代码。

现在考虑一下,当我们试图对新的 Form 对象调用,比如说 getArea() 方法时发生什么情况。因为 Form 对象中的几何形状与 if-then-else代码块中的任一测试的几何形状都不匹配,它的面积将在 else分句中被计算,好像它是个三角形似的!

这里将不会报错。事实上,在很多情况下,返回值看起来都好象是完全合理的数字。即使我们插入些冗余代码,检查 else分句中的隐含条件是否包含(比如说,断言),也要到代码执行时才能发现错误。

很多其它相似的错误也可能在上述代码中产生。 if-then-else 代码块可能会偶尔遗漏一句分句,导致类型与那句分句相对应的所有 Form 都被错误地处理了。此外,因为 impostor type 在字段中只是一个 String ,所以它可能会被意外或恶意地修改。

无论用哪一种方法,这样的修改会带来各种各样的损害。





回页首


治疗和预防措施

正如您可能设想过的那样,我建议用类型系统在静态检查期间将它们清除,从而避免这种类型的错误。请考虑这种新颖的实现方法:


清单 3. 用实际类型实现 form
public abstract class Form {
     double scale;
     public Form(double _scale) {
         this.scale = _scale;
     }
     public abstract double getArea();
 }
 class Square extends Form {
     public Square(double _scale) {
         super(_scale);
     }
     public double getArea() {
         return scale * scale;
     }
 }
 class Circle extends Form {
     public Circle(double _scale) {
         super(_scale);
     }
     public double getArea() {
         return Math.PI * scale * scale;
     }   
 }
 class Triangle extends Form {
     public Triangle(double _scale) {
         super(_scale);
     }
     public double getArea() {
         return scale * (scale * Math.sqrt(3) / 4);
     }
 }


现在考虑一下,在创建一个新 Form 时,如果误输入了“Sqaure”,会发生什么情况。编译器将会报错,告诉我们类 Sqaure 找不到。代码将连运行的机会也没有。

同样地,编译器将不会允许我们忘记为我们的任意子类定义 getArea() 方法。当然,任何对象要改变 Form 的类型是不可能的。





回页首


最后说明

在离开这个主题之前,我还想讨论另一种可能的实现,一种我曾经讨论过的两种实现方法的混合。

在这种情况下,不使用 impostor type,但代码包含很多相同的易受性,似乎它们以前就有。实际上,这种实现方法比对每个类型单独实现 getArea() 方法 更差


清单 4. 一种混合的实现方式
public abstract class Form {
     double scale;
     public Form(double _scale) {
         this.scale = _scale;
     }
     public double getArea() {
         if (this instanceof Square) {
             return scale * scale;
         }
         else if (this instanceof Circle) {
             return Math.PI * scale * scale;
         }
         else { // this instanceof Triangle
             return scale * (scale * Math.sqrt(3) / 4);
         }
     }
 }
 class Square extends Form {
     public Square(double _scale) {
         super(_scale);
     }
 }
 class Circle extends Form {
     public Circle(double _scale) {
         super(_scale);
     }
 }
 class Triangle extends Form {
     public Triangle(double _scale) {
         super(_scale);
     }
 }


尽管编译器仍旧会捕获类型的拼写错误,且对象类型是无法改变的,我们又一次使用了 if-then-else代码块调度适当的类型。这样,我们又要面临 if-then-else代码块中 instanceof检查与我们所操作的那组类型不匹配的情况。

还必须提出,像第一种实现方法那样,这个实现方法的扩展性不如第二种。





回页首


总结

那么,简而言之,这就是我们最近的错误模式:

  • 模式:Impostor Type
  • 症状:一种程序,它用同样的方式处理概念上不同类型的数据,或者无法识别某种类型的数据。
  • 起因:程序针对各种类型的数据使用带标记的字段,而不是独立的类。
  • 治疗和预防措施:尽可能将概念上不同的数据类型分成几个独立的类。

重点在于,这种语言为您提供了避免这类错误的最好资源 ― 只是要记得使用它们。



参考资料



关于作者

Eric Allen 毕业于 Cornell 大学,曾获得计算机系和数学系的学士学位。他还是 Rice 大学 Java 编程语言小组的博士研究生。它的研究涉及到开发用于 Java 语言的语义模型和静态分析工具,两者都是源代码和字节码级别的。目前,他正在为 NextGen 编程语言实现一种从源代码到字节码的编译器,这也是 Java 语言的泛型运行时类型的一种扩展。请通过 eallen@cs.rice.edu与他联系。




对本文的评价










回页首


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