# 9.5 异常的限制
覆盖一个方法时,只能产生已在方法的基类版本中定义的异常。这是一个重要的限制,因为它意味着与基类协同工作的代码也会自动应用于从基类派生的任何对象(当然,这属于基本的OOP概念),其中包括异常。
下面这个例子演示了强加在异常身上的限制类型(在编译期):
```
//: StormyInning.java
// Overridden methods may throw only the
// exceptions specified in their base-class
// versions, or exceptions derived from the
// base-class exceptions.
class BaseballException extends Exception {}
class Foul extends BaseballException {}
class Strike extends BaseballException {}
abstract class Inning {
Inning() throws BaseballException {}
void event () throws BaseballException {
// Doesn't actually have to throw anything
}
abstract void atBat() throws Strike, Foul;
void walk() {} // Throws nothing
}
class StormException extends Exception {}
class RainedOut extends StormException {}
class PopFoul extends Foul {}
interface Storm {
void event() throws RainedOut;
void rainHard() throws RainedOut;
}
public class StormyInning extends Inning
implements Storm {
// OK to add new exceptions for constructors,
// but you must deal with the base constructor
// exceptions:
StormyInning() throws RainedOut,
BaseballException {}
StormyInning(String s) throws Foul,
BaseballException {}
// Regular methods must conform to base class:
//! void walk() throws PopFoul {} //Compile error
// Interface CANNOT add exceptions to existing
// methods from the base class:
//! public void event() throws RainedOut {}
// If the method doesn't already exist in the
// base class, the exception is OK:
public void rainHard() throws RainedOut {}
// You can choose to not throw any exceptions,
// even if base version does:
public void event() {}
// Overridden methods can throw
// inherited exceptions:
void atBat() throws PopFoul {}
public static void main(String[] args) {
try {
StormyInning si = new StormyInning();
si.atBat();
} catch(PopFoul e) {
} catch(RainedOut e) {
} catch(BaseballException e) {}
// Strike not thrown in derived version.
try {
// What happens if you upcast?
Inning i = new StormyInning();
i.atBat();
// You must catch the exceptions from the
// base-class version of the method:
} catch(Strike e) {
} catch(Foul e) {
} catch(RainedOut e) {
} catch(BaseballException e) {}
}
} ///:~
```
在`Inning`中,可以看到无论构造器还是`event()`方法都指出自己会“抛”出一个异常,但它们实际上没有那样做。这是合法的,因为它允许我们强迫用户捕获可能在覆盖过的event()版本里添加的任何异常。同样的道理也适用于`abstract`方法,就象在`atBat()`里展示的那样。
`interface Storm`非常有趣,因为它包含了在`Incoming`中定义的一个方法——`event()`,以及不是在其中定义的一个方法。这两个方法都会“抛”出一个新的异常类型:`RainedOut`。当执行到`StormyInning extends`和`implements Storm`的时候,可以看到`Storm`中的`event()`方法不能改变`Inning`中的`event()`的异常接口。同样地,这种设计是十分合理的;否则的话,当我们操作基类时,便根本无法知道自己捕获的是否正确的东西。当然,假如`interface`中定义的一个方法不在基类里,比如`rainHard()`,它产生异常时就没什么问题。
对异常的限制并不适用于构造器。在`StormyInning`中,我们可看到一个构造器能够“抛”出它希望的任何东西,无论基类构造器“抛”出什么。然而,由于必须坚持按某种方式调用基类构造器(在这里,会自动调用默认构造器),所以派生类构造器必须在自己的异常规范中声明所有基类构造器异常。
`StormyInning.walk()`不会编译的原因是它“抛”出了一个异常,而`Inning.walk()`却不会“抛”出。若允许这种情况发生,就可让自己的代码调用`Inning.walk()`,而且它不必控制任何异常。但在以后替换从`Inning`派生的一个类的对象时,异常就会“抛”出,造成代码执行的中断。通过强迫派生类方法遵守基类方法的异常规范,对象的替换可保持连贯性。
覆盖过的`event()`方法向我们显示出一个方法的派生类版本可以不产生任何异常——即便基类版本要产生异常。同样地,这样做是必要的,因为它不会中断那些已假定基类版本会产生异常的代码。差不多的道理亦适用于`atBat()`,它会“抛”出`PopFoul`——从`Foul`派生出来的一个异常,而`Foul`异常是由`atBat()`的基类版本产生的。这样一来,假如有人在自己的代码里操作`Inning`,同时调用了`atBat()`,就必须捕获`Foul`异常。由于`PopFoul`是从`Foul`派生的,所以异常控制器(模块)也会捕获`PopFoul`。
最后一个有趣的地方在`main()`内部。在这个地方,假如我们明确操作一个`StormyInning`对象,编译器就会强迫我们只捕获特定于那个类的异常。但假如我们向上转换到基类型,编译器就会强迫我们捕获针对基类的异常。通过所有这些限制,异常控制代码的“健壮”程度获得了大幅度改善(注释③)。
③:ANSI/ISO C++施加了类似的限制,要求派生方法异常与基类方法抛出的异常相同,或者从后者派生。在这种情况下,C++实际上能够在编译期间检查异常规范。
我们必须认识到这一点:尽管异常规范是由编译器在继承期间强行遵守的,但异常规范并不属于方法类型的一部分,后者仅包括了方法名以及参数类型。因此,我们不可在异常规范的基础上覆盖方法。除此以外,尽管异常规范存在于一个方法的基类版本中,但并不表示它必须在方法的派生类版本中存在。这与方法的“继承”颇有不同(进行继承时,基类中的方法也必须在派生类中存在)。换言之,用于一个特定方法的“异常规范接口”可能在继承和覆盖时变得更“窄”,但它不会变得更“宽”——这与继承时的类接口规则是正好相反的。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物