发现错误的理想时机是在编译阶段,也就是在你试图运行程序之前,然而编译期间并不能找出所有的错误,余下的问题必须在运行期间解决。
使用异常所带来的的一个相当明显的好处是,它往往能够降低错误处理的复杂度,如果不使用异常就必须检查特定的错误,并在程序中的许多地方去处理它。而如果使用异常就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且只需在一个地方处理错误,即所谓的异常处理程序中。这种方式不仅节省代码,而且把"描述在正常执行过程中做什么事"的代码和"出了问题怎么办"的代码相分离。
异常使得我们可以将每件事都当作一个事务来考虑,而异常可以看护着这些事务的底线。我们还可以将异常看作是一种内建的恢复系统,因为我们在程序中可以拥有各种不同的恢复点,如果程序的某部分失败了,异常将恢复到程序中某个已知的稳定点上。
异常声明
Java鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常,这就是异常说明,异常说明使用了附加的关键字throws,它属于方法声明的一部分,紧跟在形式参数列表之后。
如果方法里的代码产生了异常却没有处理,编译器会发现这个问题并提醒你:要么处理这个异常,要么就在异常说明中表明此方法将产生异常。不过反向操作是被允许的,这样做的好处是:为异常先占个位子,以后就可以抛出这种异常而不用修改已有的代码,在定义抽象基类和接口时派生类或接口实现就能够抛出这些预先定义的异常,基于这个目的,方法抛出的异常只能是更加具体(某种程度上是异常协变类型)或者说不声明也不抛出,构造器的话因为是调用关系所以只需包含基类构造器的异常声明,自己额外加异常是被允许的。注意:异常声明虽然对于继承上做出了如上限制,但它仍然不属于方法签名的一部分,所以不能以此做重载判断。
属于运行时异常的类型有很多,它们会自动被Java虚拟机抛出,所以不必在异常说明中把它们列出来。如果RuntimeException没有被捕获而直达main(),那么在程序退出前将调用异常的printStackTrace()方法;
在多重继承中,如果基类和接口的异常声明冲突了,则在导出类声明时以基类为准。
重新抛出异常
重新抛出异常会把异常抛给上一级环境中的异常处理程序,同一个try块的后续catch子句将被忽略。
如果只是把当前异常对象重新抛出,那么printStackTrace()方法显示的将是原来异常抛出的调用栈信息,而并非重新抛出点的信息,要想更新这个信息,可以调用fillInStackTrace()方法。
public static void h() throws Excpetion {
try {
f();
} catch (Exception e) {
e.printStackTrace(System.out):
throw (Exception) e.fillInStackTrace():
}
}
有可能在捕获异常之后抛出另一种异常,这么做的话得到的效果类似于使用fillStackTrace(),有关原来的异常发生点的信息会丢失,剩下的是与新抛出点有关的信息,异常栈顶会被刷新成重新抛出的代码点,丢失的是重新抛出前的路径。
常常会想要在捕获一个异常后抛出另一个异常,并且希望把原始异常的信息保存下来,这被称为异常链,在Throwable的子类中,只有三种基本的异常类提供了带cause参数的构造器,它们是Error、Exception和RuntimeException,如果要把其他类型的异常链接起来,应该使用initCause()方法而不是构造器。
DynamicFieldException dfe = new DynamicFieldException();
dfe.initCause(new NullPointerException());
throw dfe;
finally
当要把除内存之外的资源恢复到它们的初始状态时,就要用到finally子句,这种需要清理的资源包括:已经打开的文件或网络连接等。
在finally里直接return,会把没有catch的异常直接忽视掉,不再往外抛。如果return有值,会覆盖try块里的返回值。
public class ExceptionSilencer {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
return;
}
}
}