捕获异常
如果某个异常发生的时候没有在任何地方进行捕获,那程序就会终止执行,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
要想捕获一个异常,必须设置 try/catch 语句块。如果在 try 语句块中的任何代码抛出了一个在 catch 子句中说明的异常类,那么:
- 程序将跳过 try 语句块的其余代码。
- 程序将执行 catch 子句中的处理器代码。
如果在 try 语句块中的代码没有抛出任何异常,那么程序将跳过 catch 子句。如果方法中的任何代码抛出了一个在 catch 子句中没有声明的异常类型,那么这个方法就会立刻退出。
比如,读取数据通过 try/catch 来捕获异常:
String read(String filename) {
try {
InputStream in = new FileInputStream(filename);
int b;
while((b != in.read()) != -1) {
procoess input...
}
} catch (IOException exception) {
exception.printStackTrace();
}
}
通常,最好的选择是什么也不做,而是将异常传递给调用者。如果 read 方法出现了错误,就让 read 方法的调用者去操心!如果采用这种处理方式,就必须声明这个方法可能会抛出一个 IOException :
String read(String filename) throws IOException {
InputStream in = new FileInputStream(filename);
int b;
while((b != in.read()) != -1) {
procoess input...
}
}
哪种方法更好呢?通常,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续进行传递。
如果编写一个覆盖超类的方法,而这个方法又没有抛出异常(如 JComponent 中的 paintComponent ),那么这个方法就必须捕获方法代码中出现的每一个受查异常。不允许在子类的 throws 说明符中出现超过超类方法所列出的异常类范围。
捕获多个异常
在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。
try {
code that mitght throw exception
} catch (FileNotFoundException e) {
emergency action for missing files
} catch (UnknownHostException e) {
emergency action for unknow hosts
} catch (IOException e) {
emergency action for all other I/O problems
}
也有一些 API 可以过去异常的信息:
e.getMessage(); // 获取对象的更多信息
e.getClass().getName(); // 得到异常对象的实际类型
在 Java SE 7 中,同一个 catch 子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并 catch 子句:
try {
code that mitght throw exception
} catch (FileNotFoundException | UnknownHostException e) {
emergency action for missing files
} catch (IOException e) {
emergency action for all other I/O problems
}
只有当捕获的异常类型彼此之间不存在子类关系时才需要这个特性。
捕获多个异常时,异常变量隐含为final变量。故上述的
catch (FileNotFoundException | UnknownHostException e)
中的 e 就不能在被赋不同的值。
再次抛出异常与异常链
在catch子句中可以抛出一个异常,这样做的目的是改变异常的类型。
try {
access the database
} catch (SQLException e) {
throw new ServletException("database error: " + e.getMessage());
}
上述过程会改变抛出的异常,这样做很不好,因为在检查错误的过程中不能准确的定位到真正的错误所在,更好的方式是将原始异常「包装」起来:
try {
access the database
} catch (SQLException e) {
Throwable se = new ServletException("database error: ");
se.init(Cause(e));
throw se;
}
可以使用下面这条语句得到原始异常:
Throwable e = se.getCause();
强烈建议使用这种包装技术。这样可以让用户抛出子系统中的高级异常,而不会丢失原始异常的细节。
如果在一个方法中发生了一个受查异常,而不允许抛出它,那么包装技术就十分有用。我们可以捕获这个受查异常,并将它包装成一个运行时异常。
finally 子句
不管是否有异常被捕获,finally 子句中的代码都被执行。就可以很好的处理关闭文件问题,减少资源的浪费:
InputStream in = new FileInputStream(...);
try {
code that might throw exception
} catch (IOException e) {
show error message
} finally {
in.close();
}
无论上面是正常运行,还是抛出了在 catch 子句中捕获的异常,还是在 catch 子句中未捕获的。finally 都将会运行,字节流关闭掉。
try 语句可以只有 finally 子句,而没有 catch 子句。例如,下面这条 try 语句:
InputStream in = ...
try {
code that might throw exception
} finally {
in.close();
}
无论在 try 语句块中是否遇到异常,finally 子句中的 in.close()
语句都会被执行。
在需要关闭资源时,使用 finally 子句是一种不错的选择。
上述代码还有一个冗长的写法:
InputStream in = ...
try {
try {
code that might throw exception
} finally {
in.close();
}
} catch (IOException e) {
show error message
}
内层的 try 语句块只有一个职责,就是确保关闭输入流。外层的 try 语句块也只有一个职责,就是确保报告出现的错误。这种设计方式不仅清楚,而且还具有一个功能,就是将会报告 finally 子句中出现的错误。
在 try/catch/finally 语句中,finally 是真的就是最后运行,即使遇到了 return 语句:
public static int f(int n) {
try {
int r = n * n;
return n;
} finally {
if (n == 2) return 0;
}
}
如果调用 f(2)
,那么 try 语句块的计算结果为 r=4
,并执行 return 语句。然而,在方法真正返回前,还要执行 finally 子句。finally 子句将使得方法返回 0,这个返回值覆盖了原始的返回值 4。
有时候,finally 子句也会带来麻烦。例如,清理资源的方法也有可能抛出异常。假设希望能够确保在流处理代码中遇到异常时将流关闭。
InputStream in = ...
try {
code that might throw exception
} finally {
in.close();
}
现在,假设在 try 语句块中的代码抛出了一些非 IOException 的异常,这些异常只有这个方法的调用者才能够给予处理。执行 finally 语句块,并调用 close 方法。而 close 方法本身也有可能抛出 IOException 异常。当出现这种情况时,原始的异常将会丢失,转而抛出 close 方法的异常。
这会有问题,因为第一个异常很可能更有意思。如果你想做适当的处理,重新抛出原来的异常,代码会变得极其繁琐。如下所示:
InputStream in = ...
Exception ex = null;
try {
try {
code that might throw exception
} catch (Exception e) {
ex = e;
throw e;
}
} finally {
try {
in.close();
} catch (Exception e) {
if (ex == null) throw e;
}
}
带资源的 try 语句
带资源的 try 语句可以这样写:
try (Resource res = ...) {
work with res
}
try块退出时,会自动调用 res.close()
。
比如,要读取一个文件中的所有单词:
try (Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words", "UTF-8"))) {
while (in.hasNext()) System.out.println(in.next());
}
这个块正常退出时,或者存在一个异常时,都会调用 in.close()
方法,就好像使用了 finally 块一样。
上一节已经看到,如果 try 块抛出一个异常,而且 close 方法也抛出一个异常,这就会带来一个难题。带资源的 try 语句可以很好地处理这种情况。原来的异常会重新抛出,而 close 方法抛出的异常会I「被抑制」。这些异常将自动捕获,并由 addSuppressed 方法增加到原来的异常。如果对这些异常感兴趣,可以调用 getSuppressed 方法,它会得到从 close 方法抛出并被抑制的异常列表。
分析堆栈轨迹元素
堆栈轨迹(stack trace)是一个方法调用过程的列表,它包含了程序执行过程中方法调用的特定位置。
可以调用 Throwable 类的 printStackTrace 方法访问堆栈轨迹的文本描述信息。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String descrption = out.toString();
一种更灵活的方法是使用 getStackTrace 方法,它会得到 StackTraceElement 对象的一个数组,可以在你的程序中分析这个对象数组。例如:
Throable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames)
System.out.println(frame);
StackTraceElement 类含有能够获得文件名和当前执行的代码行号的方法,同时,还含有能够获得类名和方法名的方法。toString 方法将产生一个格式化的字符串,其中包含所获得的信息。