异常
1. Java异常概述
1.1 Java异常类体系
在Java中,异常对象都是派生于Throwable
类的一个类实例。所有的异常都是由Throwable
继承而来,然后紧接着分为两大体系——Error
和Exception
:

Error
类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序中不应该抛出这种类型的异常,如果出现了,程序员几乎无能为力去解决。Exception
类是设计Java程序时要重点关注的,该分支又可分为两大分支:一个派生于RuntimeException
,另一个分支包含其他异常。一般规则是,由编程错误导致的异常属性RuntimeException
;如果程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
从职能属性上来说,Java 的异常又可划分为所谓检查型异常与非检查型异常:
检查型(checked)异常:指在编译时必须进行处理的异常,编译器将检查你是否为所有的此类异常提供了异常处理器。当方法可能抛出检查型异常时,调用该方法的代码必须使用
try-catch
块来捕获这些异常,或者在方法签名中使用throws
关键字声明该异常,表示该方法可能抛出这种异常,由调用方来处理。public Image loadImage(String s) throws FileNotFoundException, EOFException { // ... }
非检查型(unchecked)异常:非检查型异常是指在编译时不需要强制进行处理的异常。此类异常任何程序代码都有可能抛出,程序员对此完全无法控制,因此不需要在方法中声明。比如,派生于
Error
类或RuntimeException
类的异常都为非检查型异常。
所有Throwable
的子类在构造器中都可以接收一个cause
对象作为参数,用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。
1.2 异常类的方法
所有的异常对象都包含了如下几个常用方法:
方法 | 作用 |
---|---|
getMessage() | 返回该异常的详细描述字符串 |
printStackTrace() | 将该异常的跟踪栈信息输出到标准错误输出 |
printStackTrace(PrintStream s) | 将该异常的跟踪栈信息输出到指定输出流 |
getStackTrace() | 返回该异常的跟踪栈信息 |
1.3 自定义异常类
自定义异常类时,该类首先必须派生于 Throwable
。其次,通常需要提供两个构造器:
一个无参数的构造器;
一个带一个字符串参数的构造器,这个字符串将作为该异常对象的描述信息(也就是异常对象的
getMessage()
方法的返回值)。因为超类Throwable
的toString
方法会返回一个字符串,其中包含这个详细信息,这在调试中非常有用。class AuctionException extends Exception { public AuctionException() { } public AuctionException(String msg) { super(msg); } }
2. try语句块
Java异常处理机制的语法结构:
try {
// ...
} catch (SubException1 e) {
// ...
} catch (SubException2 e){
// ...
}
// ...
finally{
// ...
}
try
块与if/switch
语句不一样,try
块与catch
块后的花括号{}
永远不可省略;一个
catch
块还可以捕获多种类型的异常,此时:- 多种异常类型之间用竖线(
|
)隔开; - 异常变量有隐式的
final
修饰,因此程序不能对异常变量重新赋值; - 捕获多个异常不仅会让你的代码看起来更简单,还会更高效。生成的字节码只包含对应公共
catch
子句的一个代码块。
class MultiExceptionTest { public static void main(String[] args) { try { int a = Integer.parseInt(args[0]); int b = Integer.parseInt(args[1]); int c = a / b; } catch (IndexOutOfBoundsException | NumberFormatException | ArithmeticException ie) { System.out.println("程序发生了数组越界、数字格式异常、算法异常之一"); // 捕获多异常时,异常变量默认有final修饰,故下行错误 ie = new ArithmeticException("test"); } catch (Exception e) { System.out.println("未知异常"); // 捕获一种类型的异常时,异常变量没有final修饰,故下行正确 e = new RuntimeException("test"); } } }
- 多种异常类型之间用竖线(
对于
finally
块,其总会被执行,牢记这句话!- 即便
try/catch
块存在return/throw
语句,finally
块也会执行,当finally
块执行完成后,系统才会再次跳回来执行try/catch
块中的return/throw
语句。 - 当
finally
子句包含return
语句时,有可能产生意想不到的结果:假设利用return
语句从try
语句块中间退出,在方法返回前,会执行finally
子句块,如果finally
块也有一个return
语句,这个返回值将会遮蔽原来的返回值。 - 一个需要额外强调的情况是,如果在
finally
块执行前使用System.exit(1)
退出了虚拟机,则finally
块将失去执行的机会。 - 由上,为避免复杂的流程,强烈建议不要把改变控制流的语句(
return, throw, break, continue
)放在finally
子句中。
- 即便
进行异常捕获时,应遵循这样的规则:把Exception
类对应的catch
块放在最后,而且所有父类异常的catch
块都应该排在子类异常catch
块的后面,否则将出现编译错误。
3. 关闭物理资源
有时程序在try
块里打开了一些物理资源,如数据库连接、网络连接、磁盘文件等,这些物理资源都必须显式回收,通常在finally
块中进行回收。
try
关键字后可紧一对圆括号,圆括号可以声明、初始化一个或多个物理资源,try
语句在该语句结束时自动关闭这些资源。此时要求这些资源实现类必须实现AutoCloseable
或Closeable
接口,实现这两个接口就必须实现close()
方法。
class AutoCloseTest {
public static void main(String[] args) throws IOException {
try (
// 声明、初始化两个可关闭的资源
// try语句会自动关闭这两个资源
BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
PrintStream ps = new PrintStream(new FileOutputStream("a.txt"))) {
// 使用两个资源
System.out.println(br.readLine());
ps.println("庄生晓梦迷蝴蝶");
}
}
}
Java 9再次增强了这种try
语句,Java 9不要求在try
后的圆括号内声明并创建资源,只需要自动关闭的资源有final
修饰或者是effectively final
,Java 9允许将资源变量放在try
后的圆括号内。
class AutoCloseTest2 {
public static void main(String[] args) throws IOException {
// 有final修饰的资源
final BufferedReader br = new BufferedReader(new FileReader("AutoCloseTest.java"));
// 没有显式使用final修饰,但只要不对该变量重新赋值,该变量就是effectively final的
PrintStream ps = new PrintStream(new FileOutputStream("a.txt"));
// 只要将两个资源放在try后的圆括号内即可
try (br; ps) {
// 使用两个资源
System.out.println(br.readLine());
ps.println("庄生晓梦迷蝴蝶");
}
}
}
4. throws/throw
thorws
关键字在方法签名中使用,用于声明该方法可能抛出的异常;throw
关键字用于抛出一个实际的异常,throw
可以单独作为语句使用,抛出一个具体的异常对象。
4.1 使用throws抛出异常
throws ExceptionClass1, ExceptionClass2…
throws
意味着当前方法不知道如何处理这种类型的异常,将该异常交由上一级调用者处理。
当
main
方法使用了throws
声明抛出异常时,该异常将交由JVM处理。JVM对异常的处理方法是:打印异常的跟踪栈信息,并中止程序运行。
class ThrowsTest {
// main方法声明不处理IOException异常,将该异常交由JVM处理
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
}
方法重写时使用throws
声明抛出异常有一个限制:子类方法声明抛出的异常类型应该是父类方法声明抛出的异常类型的子类或相同,子类方法声明抛出的异常不允许比父类方法声明抛出的异常多。
class OverrideThrows {
public void test() throws IOException {
FileInputStream fis = new FileInputStream("a.txt");
}
}
class Sub extends OverrideThrows {
// 下面方法出错
public void test() throws Exception {
}
}
4.2 使用throw抛出异常
throw exceptionInstance;
throw
语句可在程序中主动抛出异常,throw
语句抛出的不是异常类,而是一个异常实例,且每次只能抛出一个异常实例。
5. 异常链
真实的企业级应用,常常有严格的分层关系,上层功能的实现严格依赖于下层的API,也不会跨层访问。

对一个上图所示结构的应用,当业务逻辑层访问持久层出现
SQLException
异常时,程序不应该把底层的SQLException
异常传到用户界面:
- 对于正常用户而言,他们不想看到底层
SQLException
异常,SQLException
异常对他们使用该系统没有任何帮助;- 对于恶意用户而言,将
SQLException
异常暴露出来不安全。
把底层的原始异常直接传给用户是一种不负责任的表现,通常的做法是:程序先捕获原始异常,然后抛出一个新的业务异常,新的业务异常中包含了对用户的提示信息,这种处理方式被称为异常转译。
异常转译可以保证底层异常不会扩散到表现层,避免向上暴露太多的实现细节。这种捕获一个异常然后抛出另一个异常,并把原始异常信息保存下来是一种典型的链式处理(23种设计模式之一:职责链模式),也被称为异常链。
所有Throwable
的子类在构造器中都可以接收一个cause
对象作为参数,这个cause
就用来表示原始异常,这样可以把原始异常传递给新的异常,使得即使在当前位置创建并抛出了新的异常,也能通过这个异常链追踪到异常最初发生的位置。
6. 异常处理规则
不要过度使用异常,过度使用异常主要有两个方面:
- 把异常和普通错误混淆在一起,不再编写任何错误处理代码,而是以简单地抛出异常来代替所有的错误处理;
- 使用异常处理来代替流程控制。
异常处理机制的初衷是将不可预期异常的处理代码和正常的业务逻辑处理代码分享,因此绝不要使用异常处理来代替正常的业务逻辑判断。
不要使用过于庞大的try
块。
避免使用CatchAll语句,即不要处理程序发生的所有可能异常:catch (Throwable t){}
不要忽略捕获到的异常。
7. 断言
断言的概念
断言是一种测试和调试阶段使用的战术性工具;与之不同,日志是一种在程序整个生命周期都可以使用的战略性工具。
断言机制允许在测试期间向代码中插入一些检查,而在生产代码中会自动删除这些检查。Java语言引入了关键字assert
,这个关键字有两种形式:
assert condition
assert condition: expression
:表达式部分的唯一目的是产生一个消息字符串,AssertionError
对象并不存储具体的表达式值,因此,以后无法得到这个表达式值。
启用和禁用断言
在默认情况下,断言是禁用的,可以在运行程序时用-enableeassertions
或-ea
选项启用断言:
java -enableassertions MyApp
需要注意的是,不必重新编译程序来启用或禁用断言。启用或禁用断言是类加载器的功能。禁用断言时,类加载器会去除断言代码,因此,不会降低程序运行的速度。
也可以在某个类或整个包中启用断言,例如:
java -ea:MyClass -ea:com.mycompany.mylib MyApp
也可以使用选项 -disableassertions
或 -da
在某个特定类和包中禁用断言:
java -ea:... -da:MyClass MyApp
8. 相关类
Throwable
StackWalker
StackWalker.StackFrame
StackTraceElement