诊断Java代码:使用静态类型的理由

静态类型 — 多数程序员喜欢它或憎恨它。支持者夸耀说静态类型让他们写出更干净更可靠的代码,没有它们则做不到这么好。批评者埋怨说静态类型增加了程序的复杂性。

是的,静态类型不是免费午餐;有时候,它们用起来很乏味。然而,如果我们主要关心的是使代码没有错误,那么,总的说来,Java 编程还是拥有并使用静态类型好些。为什么?静态类型检查:

通过早期错误检测,提高健壮性

通过在最佳的时候作所需的检查,提高性能

弥补单元测试的缺点

我们来更仔细地分析这些原因,并看一看静态类型检查和结对编程(pair programming)混用。

通过早期检测,提高健壮性

静态类型检查能提高程序健壮性。为什么?因为它有助于尽快找到错误 — 在程序运行前。这里的逻辑舍此无它。错误越早被发现,问题诊断起来就越容易,也就只有越少的数据会被错误的计算毁坏。

在程序运行前就找到并诊断错误是理想状态。这个优点使静态类型检查成为编程语言设计中的伟大成功,因为它是少数几种能在程序运行前自动检测程序中的错误的方法,而且它可在可接受的时间内完成这个任务。“可接受的时间”意思是与程序长度呈线性关系的时间(有一个很小的常数系数),而不是其它形式自动检查所要求的呈立方甚至呈指数关系的时间(许多甚至根本不保证完成)。

是的,类型系统越强大,编起程序来就越容易(且系统将检测越多的错误)。我不否认 Java 简单的类型系统留有很多缺憾;它经常阻碍我们,迫使我们用强制转型来绕过。但这种状况正在慢慢改善。

Sun JSR14 编译器在语言中增加了形式有限的泛型(也称为 参数化)类型;我们相信它迟早会被加入该语言,因为它目前在 Java 社区过程中得到了坚定的支持。更高级的语言扩展(例如 NextGen)承诺在 JSR14 提供的增加的表达力上更上一层楼。那是好事,因为在很多环境下 NextGen 都有助于减少甚至在 JSR14 中也是需要的一些增加的复杂性。请参阅 参考资料找到关于这个问题的更多信息。(除了 JSR14 链接,还有些关于 参数的多态性的文章。)

然而,静态类型检查的好处不仅仅是健壮性。它还能保护您的程序的性能。

通过减少所需的检查,提高性能

在安全的语言中(“安全”的意思是指不允许我们破坏它自己的抽象的语言),对传给方法的参数的类型作各种检查是必需的并一定得完成,对被存取的域的类型的检查也是必需的并一定得完成。如果这些检查不是静态地完成,那么它们必须在运行时完成。

进行这些所需的检查是费时的,在运行时进行这些检查的语言,其性能会相应受损。当不变量被静态地检查时,我们不必在运行时检查它,从而加快程序运行。所以,静态类型检查使我们得以写出更健壮更高效的代码。

传统上认为编译时静态类型检查是低效的。对于用 C/C++ 之类语言写的大程序来说,在文件间链接各种类型引用是很费时的,因为每次编译时,各种文件必须被合在一起生成一个大的可执行文件。但是 Java 语言完全避免了这个问题,因为类是分开编译的,在需要时装入到 JVM。没有必要把所有的引用文件链接成一个可执行文件 — 所以在编译时没有相应的放慢。

现在,我们对那些声称静态类型在单元测试环境下是不必要的人说些什么呢?

突破单元测试的限制

本专栏的老读者知道,我是单元测试的坚定支持者。对您的程序全部进行单元测试是最好的作法。然而,我首先要承认单元测试的局限性。

单元测试仅能测试程序在某次特定运行时输入某些特定数据时的行为。是的,在那次运行的狭小环境下,我们可以测试程序的 深层属性。

相对而言,类型检查检查 浅层属性,但它针对的是该程序所有可能的运行和各种可能的输入。

结合类型检查和单元测试

正如指定程序行为时,素材和单元测试互相补充一样,在确定和排除错误时,单元测试和静态类型检查互相补充。这两种排错方法的结合效果大于它们各自的效果之和。

有些设计师和程序员会提出,成熟的程序中发生的错误种类比静态类型检查能发现的要深得多;所以,他们的结论是静态类型系统的弊大于利。无疑,静态类型语言使程序更冗长,甚至阻止我们写出一些从不引起任何错误的程序。

总是有折衷。使用静态类型并不是免费的午餐。我们用静态类型语言写的程序常常要比我们不使用类型系统写的程序更复杂。但是,甚至“成熟”程序也会有那种浅层错误,成为静态类型检查的特别猎物。

即使程序中的这些浅层错误被消灭,重构也很容易就会重新产生它们。如果我们打算采纳不断重构的极端编程思想,我们将在这样的浅层错误被引入后尽快地捕获它们。(相反地,单元测试有助于捕获重构时发生的更深层错误。这两个概念的整体之和大于它们的部分之和。)在极端编程的环境下,静态类型检查效果不错。

类型检查和单元测试的矛盾

尽管如此,静态类型检查和单元测试之间仍有一个须提到的矛盾。极端编程要求我们把单元测试的编写和代码的编写交叉起来以实现那些测试。

每组单元测试有助于指定功能性的一个新方面,应写在允许我们通过那些测试的代码之前。在理想情况下,我们要在写完它们后马上编译那些测试,这样我们可以确保它们已准备就绪。

但这里有一个问题: 在我们定义测试所引用的类和方法前,新测试不能通过静态类型检查。这些类和方法可以是我们以后填充的空架子,但是除非我们有点东西,否则静态检查器会认为在测试中引用它们是没意义的。

考虑清单 1 中比较简单的示例,它显示了一个多集合(multi-set)的实现的测试类(一个抽象的数据结构):

清单 1. 一个多集合的实现的测试类

import junit.framework.*;import java.io.*;/*** A test class for MultiSet.**/public class MultiSetTest extends TestCase {  private static String W = "w";  private static String X = "x";  private static String Y = "y";  private static String Z = "z";  private static MultiSet EMPTY = new MultiSet();  private static MultiSet XY = new MultiSet(X, Y);  private static MultiSet YZ = new MultiSet(Y, Z);  private static MultiSet XYZ = new MultiSet(X, Y, Z);  private static MultiSet XYY = new MultiSet(X, Y, Y);  private static MultiSet WXY = new MultiSet(W, X, Y);  /**  * Constructor.  * @param String name  */  public MultiSetTest(String name) {   super(name);  }  /**  * Creates a test suite for JUnit to run.  * @return a test suite based on the methods in this class  */  public static Test suite() {   return new TestSuite(MultiSetTest.class);  }  private void _assertOrder(MultiSet set, String key, int value) {   assertEquals("order for key " + key, value, set.order(key));  }  public void testEmpty() {   _assertOrder(EMPTY, X, 0);   _assertOrder(EMPTY, Y, 0);   _assertOrder(EMPTY, Z, 0);  }  public void testOrder() {   _assertOrder(XY, X, 1);   _assertOrder(XY, Y, 1);   _assertOrder(YZ, Y, 1);   _assertOrder(YZ, Z, 1);  }  public void testAdd() {   MultiSet added = XY.add(YZ);   _assertOrder(added, X, 1);   _assertOrder(added, Y, 2);   _assertOrder(added, Z, 1);  }  public void testSubset() {   assertTrue(XY.subset(XYZ));   assertTrue(YZ.subset(XYZ));   assertTrue(! YZ.subset(XY));   assertTrue(! XY.subset(YZ));   assertTrue(! XYZ.subset(XY));   assertTrue(! XYZ.subset(YZ));   assertTrue(! XYY.subset(XYZ));   assertTrue(! XYZ.subset(XYY));  }  public void testSubtract() {   MultiSet XYYZ = XY.add(YZ);   assertEquals(YZ, XYYZ.subtract(WXY));   assertEquals(YZ, XYYZ.subtract(XY));   assertEquals(XY, XYYZ.subtract(YZ));   assertEquals(EMPTY, EMPTY.subtract(YZ));   assertEquals(EMPTY, YZ.subtract(YZ));  }  public void testUnion() {   assertEquals(XYZ, XY.union(YZ));  }  public void testIsEmpty() {   assertTrue(EMPTY.isEmpty());   assertTrue(! XY.isEmpty());  }}

类 MultiSet 在哪?还有方法 union() 、 isEmpty() 等等是怎么样的?

类型检查器不会比您更清楚这些类和方法的位置,所以这些代码在我的环境中可以编译,但不能在您的环境中编译。也就是说,这些代码要到您实现类 MultiSet 及所有适当的方法后才能编译。请记住,在静态类型语言中,要直到您至少为您试图测试的类和方法生成空架子之后,您才能编译新的单元测试。

面向测试的开发工具的使用能容易地缓和这个矛盾。具体地说,您需要一个这样的开发工具,它能读完一个单元测试,累计该测试通过静态类型检查所必需的类和方法引用(及适当的签名),然后生成空架子类。

如果您考虑一下这样一个开发工具会设计成什么样子,很显然,面向测试的开发工具的规划与静态类型检查器非常像,除了它只是累计记录它所需生成的空架子而不是生成错误。我们目前正在为 NextGen 实现静态检查器,它有一个正是做这件事的“空架子生成”模式。

结对编程:另一浅层错误检查

静态类型检查具有对浅层但普遍的错误的检测能力,对这一能力的另一补充是 结对编程,它是极端编程的原则之一。多个聪明的人互相检查工作是消灭许多浅层错误的好方法。

另一获得这种效果的有效方法是开放源代码编码。当代码被开放后,代码常会更健壮 — 毕竟,有不止一对程序员的两双眼睛在查看代码并寻找最微小的“gotchas”问题。正如 Eric Raymond 在他著名的“The Cathedral and the Bazaar”所说(他把它称为 Linus 定律),“如果有足够多的眼球,所有的错误都是浅层的”。

超越简单的类型检查

当然,静态类型检查是有益的,出于同样的理由,更高级形式的静态检查也是有益的。术语“静态检查”和“静态分析”是比仅仅检查类型更广泛的概念 — 它们指任何为确定程序在运行时的行为而分析程序文本的机制。

正如其他小组已证明的那样,可以扩展 Java 语言,让它包括其它形式的静态检查,例如断言(assertion)的有限静态验证。今后工作的另一方向是在 Java 语言上加入各种“软类型化系统(soft typing system)”,在软类型化系统中,像强制转型这样的运算在某些环境中可以被成功验证,但并不禁止未经验证的强制转型。

在排错时,我们应该用所有的武器来解决问题,开发新的有效的静态检查系统,以检查尽可能多的不变量。在以后的几篇文章里,我们将探讨一些可用于 Java 编程的静态分析工具,既作为原型工具,也作为生产工具。

正确的寒暄必须在短短一句话中明显地表露出你对他的关怀。

诊断Java代码:使用静态类型的理由

相关文章:

你感兴趣的文章:

标签云: