异常
所有的异常都是由Throwable继承而来的子类,分为Error和Exception两种:
Error类描述了Java运行时系统的内部错误和资源耗尽错误,应将这种异常通告给用户并尽力使程序安全地终止。
Exception分为RuntimeException和IOException两种:
RuntimeException描述由于程序错误导致的异常,例如:错误的类型转换、数组访问越界、访问空指针等等。
IOException描述非程序错误导致的异常(通常是用户输入错误导致的),例如:试图在文件尾部之后读取数据、试图打开一个不存在的文件、试图根据给定的字符串查找类对象,而字符串表示的类不存在等等。
将Error和RuntimeException称为非受查异常,而IOException称为受查异常。
声明受查异常
一个方法必须声明所有可能抛出的受查异常。而非受查异常或为不可控制的Error,或为应由程序避免发生的RuntimeException,不应当抛出。抛出异常的规范如下:
class SomeClass {
// some fields
// ...
// some methods
// ...
// method which may throw exception
public void method() throws someExcpetion {
// some code
// ...
}
}
注意:若子类覆盖了超类的某个方法,则子类中该方法不可以声明比超类更通用的异常。若超类方法没有抛出任何受查异常,则子类方法也不能抛出受查异常。
抛出异常
当一个方法声明了某个异常时,应当在异常出现时将其抛出。
class someClass {
public void method throws someException {
if (exception statements) {
throw new someException();
}
}
}
如果存在合适的异常来描述异常情况,就可以直接使用这个异常类。否则,需要自己定义异常来满足需求。
习惯上,定义的异常类应包含两个基础的构造器:默认构造器和带有详细描述信息的构造器。Throwable类的toString方法会打印这些详细信息,便于调试。
class FileFormatException extends IOException {
public FileFormatException() {}
public FileFormatException(String gripe) {
super(gripe);
}
}
捕获异常
当调用了一个抛出受查异常的方法时,必须对其进行处理,使程序能够捕获其抛出的异常或继续向外抛出该异常。
try {
// method throws exception
// ...
}catch(Exception e) {
// do something
// ...
}
注意:若超类的方法没有抛出异常,覆盖的方法也不能抛出异常,则必须捕获覆盖方法中可能出现的每一个受查异常。
异常链
当方法捕获异常而再次抛出时,应该将其包装为新的高级异常,并注意保留原始异常的细节。
以数据库异常为例:
try {
// access the database
}catch(SQLException e) {
Throwable se = new ServletException("database error");
se.initCause(e);
throw se;
}
// 捕获se时可以通过Throwable e = se.getCause()来得到原始异常
finally子句
显然try代码块与catch代码块在尾部经常包含重叠的代码。例如:资源回收处理等。可以将这些代码转移到finally子句中,这样无论是否捕获异常,这些代码都能很好地执行,且不需要重复编写。
可以通过解耦合的方式提高try/catch/finally语句块的清晰度,同时能报告finally子句中出现的错误。
InputStream in = ...;
try {
try {
// code that might throw exceptions
}
finally {
in.close();
}
}catch(Exception e) {
// show error message
}
注意:finally子句会在return 语句前执行,所以不应当在finally子句中使用return语句。
当try语句涉及资源(实现了AutoCloseable接口的类)时,try块退出或出现异常时需要在finally子句中调用AutoCloseable的close方法。但这个方法声明了异常,处理起来就会比较困难。使用带资源的try语句能很好地解决这一问题。
try(Resource res = ...) {
// work with res
}
通过上述方法编写try块,当try块正常退出或出现异常时,都会自动调用close方法,不需要使用嵌套。
否则若try块和close方法均发生异常,后者就会被自动捕获,由addSuppressed方法添加到前者中。需要调用getSuppressed方法得到从close方法抛出的被抑制的异常列表才能对这些异常进行观察,十分麻烦。
分析堆栈轨迹元素
堆栈轨迹是一个方法调用过程的列表,记录了方法调用的顺序。
可以调用Throwable类的printStackTrace方法访问堆栈轨迹的文本描述信息。
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String description = out.toString();
更灵活的方法是使用getStackTrace方法,得到堆栈轨迹元素的数组。
Throwable t= new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for (StackTraceElement frame : frames) {
// analyze frame
// ...
}
更全面地,可以使用静态方法Thread.getAllStackTrace来产生所有线程的堆栈轨迹。
Map map = Thread.getAllStackTrace();
for (Thread t : map.keySet()) {
StackTraceElement[] frames = map.get(t);
// analyze frames
// ...
}
以递归阶乘方法为例:
package exception;
import java.util.Scanner;
public class StackTraceTest {
public static int factorial(int n) {
System.out.println("factorial(" + n + "):");
Throwable t = new Throwable();
StackTraceElement[] frames = t.getStackTrace();
for(StackTraceElement frame : frames) {
System.out.println(frame);
}
int r;
if(n <= 1) r = 1;
else r = n * factorial(n-1);
System.out.println("return " + r);
return r;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
System.out.println("Enter n: ");
int n = in.nextInt();
factorial(n);
}
}
输入结果如下:
factorial(3):
exception.StackTraceTest.factorial(StackTraceTest.java:9)
exception.StackTraceTest.main(StackTraceTest.java:25)
factorial(2):
exception.StackTraceTest.factorial(StackTraceTest.java:9)
exception.StackTraceTest.factorial(StackTraceTest.java:16)
exception.StackTraceTest.main(StackTraceTest.java:25)
factorial(1):
exception.StackTraceTest.factorial(StackTraceTest.java:9)
exception.StackTraceTest.factorial(StackTraceTest.java:16)
exception.StackTraceTest.factorial(StackTraceTest.java:16)
exception.StackTraceTest.main(StackTraceTest.java:25)
return 1
return 2
return 6
异常机制技巧
不应用异常处理代替简单的测试,会造成性能的浪费。
不应过分细化异常,会导致代码量膨胀,难以排除错误。
应使用最合适的异常类型,积极使用子类异常和自定义类异常。
不应压制异常,可能导致更加严重的错误。
应当尽早抛出存在的异常。
应灵活地包装并传递异常。