1.4 优化的异常处理

这一节将要介绍的是Java语言中的异常处理。相信大部分开发人员对于Java语言中使用try-catch-finally语句块进行异常处理的基本方式都有所了解。异常处理以一种简洁的方式表示了程序中可能出现的错误,以及应对这些错误的处理方式。适当地使用异常处理技术,可以提高代码的可靠性、可维护性和可读性。但是如果使用不当,就会产生相反的效果。比如虽然一个方法声明了会抛出某个异常,但是使用这个方法的代码在异常发生的时候,却只能捕获完异常之后就直接忽略它,无法做其他的处理。而为了能够通过编译,又不得不加上catch语句。这势必会造成冗余无用的代码,同时给出不适当的异常设计的一个信号。类似这种错误使用异常的例子在日常开发中还有很多。在Java标准库中同样也有设计失败的异常处理的例子。

Java 7对异常处理做了两个重要的改动:一个是支持在一个catch子句中同时捕获多个异常,另外一个是在捕获并重新抛出异常时的异常类型更加精确。本节的内容并不限于介绍Java 7中关于异常处理的这两个新特性,还会围绕整个异常处理进行展开。这样安排的目的是帮助读者深入理解与Java的异常处理相关的内容。

1.4.1 异常的基础知识

Java语言中基本的异常处理是围绕try-catch-finally、throws和throw这几个关键词展开的。具体来说,throws用来声明一个方法可能抛出的异常,对方法体中可能抛出的异常都要进行声明;throw用来在遇到错误的时候抛出一个具体的异常;try-catch-finally则用来捕获异常并进行处理。Java中的异常有受检异常和非受检异常两类。

1.受检异常和非受检异常

在异常处理的时候,都会接触到受检异常(checked exception)和非受检异常(unchecked exception)这两种异常类型。非受检异常指的是java.lang.RuntimeException和java.lang.Error类及其子类,所有其他的异常类都称为受检异常。两种类型的异常在作用上并没有差别,唯一的差别就在于使用受检异常时的合法性要在编译时刻由编译器来检查。正因为如此,受检异常在使用的时候需要比非受检异常更多的代码来避免编译错误。

一直以来,关于在程序中到底是该使用受检异常还是非受检异常,开发者之间一直存在着争议,毕竟两类异常都各有优缺点。受检异常的特点在于它强制要求开发人员在代码中进行显式的声明和捕获,否则就会产生编译错误。这种限制从好的方面来说,可以防止开发人员意外地忽略某些出错的情况,因为编译器不允许出现未被处理的受检异常;从不好的方面来说,受检异常对程序中的设计提出了更高的要求。不恰当地使用受检异常,会使代码中充斥着大量没有实际作用、只是为了通过编译而添加的代码。而非受检异常的特点是,如果不捕获异常,不会产生编译错误,异常会在运行时刻才被抛出。非受检异常的好处是可以去掉一些不需要的异常处理代码,而不好之处是开发人员可能忽略某些应该处理的异常。一个典型的例子是把字符串转换成数字时会发生java.lang.NumberFormatException异常,忽略该异常可能导致一个错误的输入就造成整个程序退出。

目前的主流意见是,最好优先使用非受检异常。

2.异常声明是API的一部分

这一条提示主要是针对受检异常的。在一个公开方法的声明中使用throws关键词来声明其可能抛出的异常的时候,这些异常就成为这个公开方法的一部分,属于开放API。在维护这个公开API的时候,这些异常有可能会对API的演化造成阻碍,使得编写代码时不得不考虑向后兼容性的问题。

如果公开方法声明了会抛出一个受检异常,那么这个API的使用者肯定已经使用了try-catch-finally来处理这个异常。如果在后面的版本更新中,发现该API抛出这个异常是不合适的,也不能直接把这个异常的声明删除。因为这样会造成之前的API使用者的代码无法通过编译。

因此,对于API的设计者来说,谨慎考虑每个公开方法所声明的异常是很有必要的。因为一旦加了异常声明,在很长的一段时间内都无法甩掉它。这也是为什么推荐使用非受检异常的一个重要原因,非受检异常不需要声明就可以直接抛出。但是对于一个方法会抛出的非受检异常,也需要在文档中进行说明。