异常
程序运行期间不可避免会出现错误,可归为用户输入错误、设备错误、物理限制、代码错误等情况。当出现错误而使得某些操作无法完成时,程序应该具备以下能力:
-
返回到一种安全状态,让用户能执行其他命令
-
允许用户保存操作结果,妥善终止程序
异常处理(exception handing)的任务就是在错误发生时将控制权转移到合适的地方。Java 提供了异常处理机制,当某个方法不能正常完成它的任务时,可以通过另外一个路径退出方法,抛出(throw)一个封装了错误信息的对象,异常处理机制会搜索能够处理该异常状况的异常处理器(exception handler)。
一、异常分类
下图是 Java 异常层次结构的简化:
所有的异常都是从 Throwable 继承而来,在下一层分解为两个分支:Error 和 Exception,其中 Error 类层次结构描述了 Java 运行时系统的内部错误和资源耗尽错误,这种情况很少出现,不是我们必须关注的,一旦出现就只能尽量让程序安全终止。我们需要关注的是 Exception 层次结构,它又分解为两个分支:一个派生于 RuntimeException,另一个包含其他异常。
RuntimeException
如何界定?
-
RuntimeException(运行时异常):由程序错误导致的异常
-
其他异常:程序本身没有问题,像 I/O 错误这类问题导致的异常
派生于 RuntimeException 的异常包括但不限于下面几种情况:
-
ClassCastException / 类型转换错误
-
ArrayIndexOutOfBoundsException / 数组访问越界
-
NullPointerException / 访问 null 引用
其他异常则比如:
-
IOException / 试图在文件尾部之后读取数据
-
FileNotFoundException / 试图打开不存在的文件
-
ClassNotFoundException / 试图根据给定的(不正确)字符串查找 Class 对象
unchecked / checked
对于运行时异常,应该由编写程序的人解决,因为它只取决于代码本身。而其他异常还会受到不可预估的环境影响,比如一个文件可能在我检查它是否存在之前就已经被删除了。故此,把属于 Error 和 RuntimeException 的异常称为非受查异常(unchecked),其他异常则称为受查异常(checked),所谓受查,指的是编译器会检查是否为它们提供了对应的异常处理器。
二、抛出受查异常
throws
方法应该在其首部用 throws 关键字声明所有可能抛出的受查异常,作用在于,告知这个方法的调用者,并交由其处理,如果这个方法的调用者不对它进行处理,将无法通过编译。而非受查异常要么不可控制(Error),要么就应该避免发生(RuntimeException)。
声明异常的方式如下:
public FileInputStream(String name) throws FileNotFoundException {...} // 可能抛出多个 public Image loadImage(String name) throws FileNotFoundException, EOFException {...}
如果在子类中覆盖了超类的一个方法,那么子类方法中声明的异常不能比超类所声明的更加通用,也就是说,要么更具体,要么不声明。如果超类方法没有声明会抛出任何异常,那么子类在覆盖它时也不能声明。
注意,throws 只是说明了发生某种异常的可能,只有在方法执行的过程中确实发生异常时,才会把相应的异常对象传递给这个方法的调用者。
throw
对于已经存在的异常类,要将其抛出,只需要三步:
-
找到一个合适的异常类
-
创建它的一个对象
-
抛出(throw)
下面来看看抛出一个异常的具体例子,有一个名为 readData 的方法正在读取一个文件,这个文件在首部指出了其内容的长度为 1000 个字符,然而在读到 700 个字符后,文件就结束了。这可能会导致后面的程序出现不正确的结果,所以我希望在这里抛出一个异常:
throw new EOFException(); // or: // EOFException e = new EOFException(); // throw e;
EOFException 还有一个接收 String 参数的构造器,可以更具体地描述异常情况,把这一部分组合到原先的代码中:
String readData(Scanner in) throws EOFException { // ... while (...) { if (!in.hasNext()) { String detailOfTheException = "Content-length: " + len + ", Received: " + n; if (n < len) throw new EOFException(detailOfTheException); } // ... } return ... }
自定义异常类
有时候我们会遇到某些问题,无法使用标准的异常类恰当地描述,于是就可以定义自己的异常类(派生于 Exception 或其子类):
class FileFormatException extends IOException { public FileFormatException() {} public FileFormatException(String info) { super(info); } }
三、捕获异常
如果有某个异常发生了,但没有进行捕获,那么程序就会终止执行,并在控制台打印出异常信息(异常的类型和堆栈内容)。要想捕获一个异常,必须使用 try / catch 语句块。
try / catch
简单形式:
try { // your code here } catch (ExceptionType e) { // handler for this type }
如果 try 语句块的某处代码抛出了一个异常,并且符合 catch 子句中指出的类型,那么程序将:
-
跳过 try 语句块中的其余代码
-
执行 catch 子句中的处理器代码
具体例子:
public void readData(String filename) { try { InputStream in = new FileInputStream(filename); int nextByte; while (nextByte = in.read() != -1) { ... } } catch (IOException exception) { exception.printStackTrace(); } }
综合以上,如果调用了一个抛出受查异常的方法,那么就只能有两个选择:要么继续传递(throws),要么就地捕获并处理(try / catch)。哪种选择更好呢?一般来说,对那些知道如何处理的异常应该捕获和处理,而对不知道(可能了解的信息还不足)如何处理的异常应该继续传递。当然了,这是一个灵活的选择,重要的是对当前编写的方法功能有清晰的定位。
捕获多个异常
一个 try 语句块中可能会出现多个异常类型,假如想对不同类型的异常做出不同处理,可以为每个异常类型配对一个单独的 catch 子句:
try { // your code here } catch (FileNotFoundException e) { // for missing files } catch (UnknownHostException e) { // for unknowm hosts } catch (IOException e) { // for other I/O problems }
如果对某几类异常的处理是相同的,可以放在一起,合并 catch 子句,并且生成的字节码只包含一个对应公共 catch 子句的代码块(会更高效),写法如下:
try { // your code here } catch (FileNotFoundException | UnknownHostException e) { // ... } catch (IOException e) { // ... }
可以在 catch 子句中再次 throw 一个异常,目的在于改变异常的类型,提高灵活性,比如:
try { // access the database } catch (SQLException e) { throw new ServletException("database error: " + e.getMessage()); }
try { // ... } catch (SQLException e) { Throwable se = new ServletException("data error"); se.initCause(e); }
这样,当捕获到异常时,就可以通过下面的语句重新得到之前的异常:
Throwable previousException = se.getCause();
有时候可能只想记录一个异常,而不想做任何改变,重新抛出:
try { // ... } catch (Exception e) { logger.log(level, message, e); throw e; }
四、处理资源
在 try 子句中发生异常后,剩余的代码就不会执行,这就给资源回收带来了问题,比如在读取一个本地文件时出现 IOException ,方法终止,但此时输出流还未被关闭。为了处理资源回收的问题,可以有两个选择:
-
-
使用带资源的 try 语句
finally
不管是否有异常被捕获,finally 子句中的代码一定会被执行:
InputStream in = new FileInputStream(...); try { // might throw exceptions } catch (IOException e) { // 'catch' block is not neccessary } finally { in.close(); // other things that must be done }
注意:
-
当 finally 块包含 return 语句时,会覆盖原本的值
-
当 finally 块中也抛出异常时,会覆盖原始的异常(如果有)
解耦合
为了提高代码的清晰度,可以这么做:
InputStream in = ...; try { // --> 确保异常被处理 try { // --> 确保资源被释放 ... } finally { in.close(); } } catch (IOException e) { ... }
try-with-resources
如果 try 块中用到的资源属于一个实现了 AutoCloseable 接口的类,这个接口有一个 close 方法,那么就可以使用带有资源的 try 语句:
try (Scanner in = new Scanner(new FileInputSream("...")),"UTF-8") { while (in.hasNext()) { // ... } }
try (Scanner in = new Scanner(...); PrintWriter out = new PrintWriter(...)) { // ... }
使用带资源的 try 语句还有一个好处:如果 try 块和 close 方法都抛出了异常,那么 close 抛出的异常会被抑制,并自动通过 addSuppressed 方法增加到原来的异常,之后可以调用 getSuppressed 方法查看它。
API - java.lang.Throwable
-
getMessage
-
initCause
-
getCause
-
addSuppressed
-
getSuppressed
-
printStackTrace
- getStackTrace
五、堆栈轨迹
Throwable t = new Throwable(); StringWriter out = new StringWriter(); // prints object t and its backtrace to the specified print stream t.printStackTrace(out); String description = out.toString();
还有一种更灵活的方法是使用 getStackTrace,它会得到 StackTraceElement 对象的一个数组,便于在接下来进行分析:
Throwable t = new Throwable(); StackTraceElement[] stackTraceElements = t.getStackTrace(); for (StackTraceElement element : stackTraceElements) { // to analyze }
API - java.lang.StackTraceElement
-
getFileName
-
getLineNumber
-
getClassName
-
getMethodName
-
isNativeMethod