Java 异常层次结构
Error 类的异常为 Java 运行时的系统内部错误导致,如果发生了这种异常,能通知给用户做做善后工作就已经不错了,也无力回天。所以这种异常在开发时基本是无需关心,也不能抛出的。
Exception 分支下的 RuntimeException 和 IOException 是按两种规则来划分的:
- 由程序错误导致的异常属于 RuntimeException,像错误的类型转换、数组越界、空指针之类的
- 程序本身没有问题,但由于像 I/O 错误这类问题导致的异常属于 IOException
如果出现了 RuntimeException,那么一定是你的问题。
Java 语言规范中,将所有派生于 Error 或 RuntimeException 类的异常称为 Unchecked Exception,所有其他异常称为 Checked Exception。
异常的抛出
throw new EOFException();
有一点需要注意,不允许在子类的 throws 说明符中出现超过超类方法所列出的异常类范围。
方法中的异常声明
一个方法,必须声明所有的 Checked Exception。其他异常,要么不可控制(Error),要么就应该避免发生(RuntimeException)。
public Image loadImage(String s) throws FileNotFoundException, EOFException {
...
}
不过语法上并没有禁止在方法中声明 Unchecked Exception,虽然这个习惯很不好。
异常类的创建
习惯上应该定义两个构造器,如下:
class CustomException extends IOException {
public CustomException() {}
public CustomException(String message) {
super(message);
}
}
异常捕获
如果调用了一个抛出 Checked Exception 的方法,就必须对它进行处理,或者将其继续传递。
通常情况下,应该捕获那些知道如何处理的异常,而将那些不知道怎样处理的异常继续传递。将异常直接交给能够胜任的处理器进行处理比压制它更好。
异常中信息的获取
- 获取信息:
e.getMessage()
- 获取异常对象的实际类型:
e.getClass().getName()
多个异常类的 catch
try {
...
} catch (FileNotFoundException | UnknownHostException e) {
...
}
这里的异常变量 e 是 final 变量,不能在子句体中为 e 赋不同的值。
异常的再次抛出
通常在 catch 子句中抛出异常是为了改变异常的类型,一般来说,用于对第三方系统的错误进行包装。
除去直接抛出新的异常之外,更好的实践是,将原始异常设置为新异常的原因,示例如下:
public static void main(String[] args) {
try {
exception();
} catch (FileNotFoundException e) {
System.out.printf("message: %sn", e.getMessage());
Throwable origin = e.getCause();
System.out.printf("origin class: %s, message: %sn",
origin.getClass().getName(), origin.getMessage());
}
}
public static void exception() throws FileNotFoundException {
try {
throw new EOFException("origin exception");
} catch (EOFException e) {
FileNotFoundException se = new FileNotFoundException("new exception");
se.initCause(e);
throw se;
}
}
运行结果为:
message: new exception
origin class: java.io.EOFException, message: origin exception
带资源的 try 语句
通常来说,对于携带资源的语句,可以使用 finally 语句来保证无论如何都将这个资源关闭。但是考虑下面的情况:
在 try 块中抛出了一个调用者需要处理的异常,但在最后执行 finally 中的语句时,close 方法又抛出了一个异常,那么原来需要被处理的异常就被覆盖掉了,调用者接收到的变成了 close 方法的异常。
如果还是用 finally 的方法去解决,代码会变的很繁琐,但是带资源的 try 语句可以很好的解决上面的问题。
假设资源属于一个实现了 AutoCloseable 接口的类,这个接口有一个方法:
void close() throws Exception
那么就可以使用如下的 try 语句:
try (Resource res = ...) {
}
当 try 退出时,将会自动调用 res.close()
。如果 try 块抛出了异常,close 方法也抛出了一个异常,在新的方式下,原来的异常会重新抛出,而 close 方法的异常会“被抑制”。
当然如果也希望获取 close 中的那些异常,可以通过调用 getSuppressed
方法来拿到被抑制的异常列表。
异常的最佳实践
- 异常处理不能代替简单的测试。因为异常处理要消耗很多性能,简单的测试要快得多
- 适度的控制异常块的范围。既不要一个 try 块走到底,也不要一句话一个 try
- 利用异常的层次结构。不要为逻辑错误抛出 Checked Exception,如果有更合适的转换对象,那么不要犹豫
- 不要压制异常。墨菲定律,可能发生的一定会发生
- 在检测错误的时候,苛刻比放任更好。比如因为无效参数,栈空了,那么抛出一个 EmptyStackException 会比以后某个地方抛出了 NullPoinerException 要好得多
- 不要羞于传递异常。如果你不能在自己这里完美的解决,那么传递出去,让上层解决,也许告知用户或放弃命令会更加合适