【IT168 技术文章】Java 语言避免了很多支持多继承语言所特有的问题 ―部分是因为它把对接口不变量的规定限制在类型说明上。但这个限制与糟糕的文档结合会产生问题:如果对接口的实现做了错误的假设,那么在运行时会碰到令人沮丧的错误。
Java 语言接口是一种强大的工具。它具有多继承的很多优点,而没有什么问题。为客户希望使用的所有服务指定一个接口,使得在需要时插进这种接口的不同实现成为可能。
遗憾的是,规范中可以被表达的部分只有方法说明。对任何实现来说,很可能还有很多其它不变量希望被掌握,但是 Java 语言没有提供检查它们的工具。
臆想错误模式
由于这种限制,很可能“实现”了一个接口而实际上没有满足预期的语义。由这种 Fictitious Implementation导致的错误就是本周专栏的主题。
例如,请看一看下面这个堆栈的接口:
清单 1. 堆栈的接口
2 public Object pop();
3 public void push(Object top);
4 public boolean isEmpty();
5 }
6
从 Java 类型检查器的角度看,包含符合如上说明的方法的任何类可以作为 Stack 的合法实现。但是实际上,我们希望堆栈能满足一些另外的要求。例如:
如果一个对象 o 被压入堆栈 s ,并且在堆栈上进行的下一步操作是 pop ,那么这个操作的返回值应该是 o 。
如果对于一个给定的堆栈 s , s.isEmpty() 的返回值是 true ,并且在这个堆栈上进行的下一步操作是 pop ,那么调用 pop 应该抛出一个 RuntimeException 异常。
还有大量其它的可以指定的不变量。我们希望堆栈怎么处理多次 push 操作?对于多线程会有什么行为?很难通过编程来实施这些不变量。我们可以(并且应该)在文档编写时提及它们,但是编写实现的开发者可能容易忽略它们。如果发生这种情况,那么依赖这些不变量的客户将不能完成这种实现,就形成了错误。我称这种模式的错误为 Fictitious Implementation ,因为我公正地将其归咎于实现而不是客户。正如任何错误都有自己的模式一样,Fictitious Implementation 可能不能立刻看出,而是潜伏,一直隐藏到某种不平常的执行路径发现它。
不要责怪 Java 语言!
在继续这篇专栏前,我要指出我并不是批评 Java 语言不能指定这种不变量。允许这种规范的任何机制都会有很多随之而来的缺点。首先,我们想要指定的很多不变量不能被静态地检查。虽然类型说明只表达了不变量的一小部分,但是比上面我们概述的用于堆栈的这类约束容易检查。
在接口中允许更多可表达规范有另一方面的缺点:这样做,很容易让 Java 语言背负很多问题,使得语言中到处都是多继承。请看下面的接口:
清单 2. 弹出器接口
2 public Object pop();
3 }
4
假设这个接口的 pop() 方法有预期的不变量:调用 pop() 绝不会抛出一个 RuntimeException 异常。现在,利用当前 Java 的接口功能,一个类可能同时实现 Stack 和 Popper 。但是,根据预期的规范,每一个类中的 pop() 实现是相互不兼容的。
如果想继续让一个类同时实现这两种接口,还必须能够提供重载方法。这种方法不仅基于类型说明,而且基于给这种语言添加的额外的不变量规范的种类。但是这将引入一个严重的问题:一个给定的方法调用,我们怎么确定调用这种方法的哪一个版本?通常,这是通过确定方法参数的静态类型来完成;但是有了额外的不变量,这种技巧就不行了。这种问题对于常进行多继承编程的程序员来说很熟悉:如果一个以上的父类定义的方法带有相同名称和类型说明,怎样消除调用这些方法的歧义呢?有很多可以手工消除这种调用歧义的方法,但是都易使语言的复杂性大大增加。更糟糕的是,自动地消除这种调用歧义的方案本身很复杂,且易产生错误,当预测哪个方法将被调用时,程序员经常犯错。
因此,选择将接口不变量的规范限制到类型说明是完全合理的设计。我们没有必要为了检测和更正Fictitious Implementation 而放松这种限制。
检测Fictitious Implementation
当然,Fictitious Implementation 的主要问题是它能顺利通过编译。运行时的症状通常会令人非常莫名其妙,因为程序员期望接口满足的额外的不变量经常不被表示出来;程序员甚至可能没有意识到正希望满足这些不变量。更正错误的过程通常以一个混乱的阶段开始;为Fictitious Implementation 模式所绊的程序员可能首先试图说服他自己他所看到的问题不可能出现。如果您发现自己正处于这种状况,最好检查一下您的前提。您没有提到哪些隐藏的假设呢?怎么能测试这些假设来彻底消除它们是错误的可能性呢?如果您依赖于到系统另一部分的接口,并且该接口的实现自最近一次发布已经修改过了,那么您可能会碰到Fictitious Implementation 。
解决方法
在这样一些情况下,接口的维护人员将任何可由客户程序员假设的不变量 做成文档是很重要的。事实上,如果客户程序员发现他所依赖的不变量没有做成文档,那么客户程序员和接口的维护人员应该坐下来讨论一下是否应该让这种不变量明确些。通常,将一个假设的不变量添加到规范很容易,省去了客户修改所有依赖这些不变量的代码的麻烦。
如果接口的维护人员不在,那么客户只能依赖已经做成文档的接口不变量。如果缺乏这些,接口远远达不到它所应有的价值。如果客户选择依赖没有做成文档的不变量,所写的客户代码本身可能很快失去价值,因为它可能与接口实现的未来的发行版不兼容。