捕获异常

如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
要想捕获一个异常,必须设置 try/catch 语句块。如果在 try 语句块中的任何代码抛出了一个在 catch 子句中说明的异常类,那么:

  1. 程序将跳过 try 语句块的其余代码。
  2. 程序将执行 catch 子句中的处理器代码。

如果在 try 语句块中的代码没有抛出任何异常,那么程序将跳过 catch 子句。如果方法中的任何代码抛出了一个在 catch 子句中没有声明的异常类型,那么这个方法就会立刻退出。
比如,读取数据通过 try/catch 来捕获异常:

  1. String read(String filename) {
  2. try {
  3. InputStream in = new FileInputStream(filename);
  4. int b;
  5. while((b != in.read()) != -1) {
  6. procoess input...
  7. }
  8. } catch (IOException exception) {
  9. exception.printStackTrace();
  10. }
  11. }

通常,最好的选择是什么也不做,而是将异常传递给调用者。如果 read 方法出现了错误,就让 read 方法的调用者去操心!如果采用这种处理方式,就必须声明这个方法可能会抛出一个 IOException :

  1. String read(String filename) throws IOException {
  2. InputStream in = new FileInputStream(filename);
  3. int b;
  4. while((b != in.read()) != -1) {
  5. procoess input...
  6. }
  7. }

哪种方法更好呢?通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。

如果编写一个覆盖超类的方法,而这个方法又没有抛出异常(如 JComponent 中的 paintComponent ),那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的 throws 说明符中出现超过超类方法所列出的异常类范围。

捕获多个异常

在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。

  1. try {
  2. code that mitght throw exception
  3. } catch (FileNotFoundException e) {
  4. emergency action for missing files
  5. } catch (UnknownHostException e) {
  6. emergency action for unknow hosts
  7. } catch (IOException e) {
  8. emergency action for all other I/O problems
  9. }

也有一些 API 可以过去异常的信息:

  1. e.getMessage(); // 获取对象的更多信息
  2. e.getClass().getName(); // 得到异常对象的实际类型

在 Java SE 7 中,同一个 catch 子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并 catch 子句:

  1. try {
  2. code that mitght throw exception
  3. } catch (FileNotFoundException | UnknownHostException e) {
  4. emergency action for missing files
  5. } catch (IOException e) {
  6. emergency action for all other I/O problems
  7. }

只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。

捕获多个异常时,异常变量隐含为final变量。故上述的 catch (FileNotFoundException | UnknownHostException e) 中的 e 就不能在被赋不同的值。

再次抛出异常与异常链

在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。

  1. try {
  2. access the database
  3. } catch (SQLException e) {
  4. throw new ServletException("database error: " + e.getMessage());
  5. }

上述过程会改变抛出的异常,这样做很不好,因为在检查错误的过程中不能准确的定位到真正的错误所在,更好的方式是将原始异常「包装」起来:

  1. try {
  2. access the database
  3. } catch (SQLException e) {
  4. Throwable se = new ServletException("database error: ");
  5. se.init(Cause(e));
  6. throw se;
  7. }

可以使用下面这条语句得到原始异常:

  1. Throwable e = se.getCause();

强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。

如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个受查异常,并将它包装成一个运行时异常。

finally 子句

不管是否有异常被捕获,finally 子句中的代码都被执行。就可以很好的处理关闭文件问题,减少资源的浪费:

  1. InputStream in = new FileInputStream(...);
  2. try {
  3. code that might throw exception
  4. } catch (IOException e) {
  5. show error message
  6. } finally {
  7. in.close();
  8. }

无论上面是正常运行,还是抛出了在 catch 子句中捕获的异常,还是在 catch 子句中未捕获的。finally 都将会运行,字节流关闭掉。
try 语句可以只有 finally 子句,而没有 catch 子句。例如,下面这条 try 语句:

  1. InputStream in = ...
  2. try {
  3. code that might throw exception
  4. } finally {
  5. in.close();
  6. }

无论在 try 语句块中是否遇到异常,finally 子句中的 in.close() 语句都会被执行。

在需要关闭资源时,使用 finally 子句是一种不错的选择。

上述代码还有一个冗长的写法:

  1. InputStream in = ...
  2. try {
  3. try {
  4. code that might throw exception
  5. } finally {
  6. in.close();
  7. }
  8. } catch (IOException e) {
  9. show error message
  10. }

内层的 try 语句块只有一个职责,就是确保关闭输入流。外层的 try 语句块也只有一个职责,就是确保报告出现的错误。这种设计方式不仅清楚,而且还具有一个功能,就是将会报告 finally 子句中出现的错误。
在 try/catch/finally 语句中,finally 是真的就是最后运行,即使遇到了 return 语句:

  1. public static int f(int n) {
  2. try {
  3. int r = n * n;
  4. return n;
  5. } finally {
  6. if (n == 2) return 0;
  7. }
  8. }

如果调用 f(2) ,那么 try 语句块的计算结果为 r=4,并执行 return 语句。然而,在方法真正返回前,还要执行 finally 子句。finally 子句将使得方法返回 0,这个返回值覆盖了原始的返回值 4。
有时候,finally 子句也会带来麻烦。例如,清理资源的方法也有可能抛出异常。假设希望能够确保在流处理代码中遇到异常时将流关闭。

  1. InputStream in = ...
  2. try {
  3. code that might throw exception
  4. } finally {
  5. in.close();
  6. }

现在,假设在 try 语句块中的代码抛出了一些非 IOException 的异常,这些异常只有这个方法的调用者才能够给予处理。执行 finally 语句块,并调用 close 方法。而 close 方法本身也有可能抛出 IOException 异常。当出现这种情况时,原始的异常将会丢失,转而抛出 close 方法的异常。
这会有问题,因为第一个异常很可能更有意思。如果你想做适当的处理,重新抛出原来的异常,代码会变得极其繁琐。如下所示:

  1. InputStream in = ...
  2. Exception ex = null;
  3. try {
  4. try {
  5. code that might throw exception
  6. } catch (Exception e) {
  7. ex = e;
  8. throw e;
  9. }
  10. } finally {
  11. try {
  12. in.close();
  13. } catch (Exception e) {
  14. if (ex == null) throw e;
  15. }
  16. }

带资源的 try 语句

带资源的 try 语句可以这样写:

  1. try (Resource res = ...) {
  2. work with res
  3. }

try块退出时,会自动调用 res.close()
比如,要读取一个文件中的所有单词:

  1. try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words", "UTF-8"))) {
  2. while (in.hasNext()) System.out.println(in.next());
  3. }

这个块正常退出时,或者存在一个异常时,都会调用 in.close() 方法,就好像使用了 finally 块一样。
上一节已经看到,如果 try 块抛出一个异常,而且 close 方法也抛出一个异常,这就会带来一个难题。带资源的 try 语句可以很好地处理这种情况。原来的异常会重新抛出,而 close 方法抛出的异常会I「被抑制」。这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。如果对这些异常感兴趣,可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表。

分析堆栈轨迹元素

堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息。

  1. Throwable t = new Throwable();
  2. StringWriter out = new StringWriter();
  3. t.printStackTrace(new PrintWriter(out));
  4. String descrption = out.toString();

一种更灵活的方法是使用 getStackTrace 方法,它会得到 StackTraceElement 对象的一个数组,可以在你的程序中分析这个对象数组。例如:

  1. Throable t = new Throwable();
  2. StackTraceElement[] frames = t.getStackTrace();
  3. for (StackTraceElement frame : frames)
  4. System.out.println(frame);

StackTraceElement 类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。toString 方法将产生一个格式化的字符串,其中包含所获得的信息。