一.什么是异常
异常就是发生在程序的执行期间,破坏程序指令的正常流程的事件。当方法中出现错误时,该方法会创建一个对象并将其交给运行时系统。该对象称为异常对象,它包含有关错误的信息,包括错误的类型和出现错误时程序的状态。创建异常对象并将其交给运行时系统的行为称为抛出异常。
在方法抛出异常后,运行时系统会尝试在调用栈中查找可以处理它的程序。调用栈是指从最开始的方法到出现错误的方法以及之间的所有方法列表,下图是一个调用栈:
运行时系统在调用栈中查找包含可以处理异常的代码块的方法。这个代码块称为异常处理器。搜索从发生错误的方法开始,并按照调用方法的相反顺序继续查找。找到适当的处理程序后,运行时系统会将异常传递给处理程序。如果抛出的异常对象的类型与处理程序可以处理的类型匹配,就认为异常处理程序是合适的。
如果运行时系统穷举搜索调用栈上的所有方法而没有找到适当的异常处理程序,如下图所示,则运行时系统(以及程序)就会终止,并在控制台上打印出异常信息,其中包括异常的类型和堆栈的内容。
二.异常的分类
在Java中,异常对象都是派生于Throwable类的一个实例。如果Java中内置的异常类不能满足需求,用户可以创建自己的异常类。
下面是Java异常层次结构的一个示意图:
需要注意的是,所有的异常都是由Throwable继承而来,但在下一层立即分解为两个分支:Error和Exception。
Error类层次结构描述了Java运行时系统的内部错误和资源耗尽错误。应用程序不该抛出这种类型的对象。如果出现了这样的内部错误,除了通知用户,并尽力使程序安全地终止之外,便再也无能为力了。这种情况很少出现,也不需要我们关心。
在设计Java程序时,需要关注Exception层次结构。这个层次结构又分解为两个分支:一个分支派生于RuntimeException;另一个分支包含其他异常。划分两个分支的规则是:由程序错误导致的异常属于RuntimeException;而程序本身没有问题,但由于像I/O错误这类问题导致的异常属于其他异常。
常见的RuntimeException有NullPointerException、ClassCastException、NumberFormatException、IndexOutOfBoundException等,这些异常都是由于编写代码时考虑地不全面而导致的。“如果出现RuntimeException异常,那么就一定是你的问题”是一条相当有道理的规则。例如,应该通过检测数组下标是否越界来避免数组下标越界异常,应该通过在使用变量之前检测是否为null来杜绝空指针异常的发生。
还有一些异常,并不是由于代码的问题。例如,当我们删除文件时,有可能这个文件并不存在,这时候就会抛出一个异常。与RuntimeException不同,此时我们并不需要修改原有的代码,但是我们可以在抛出异常后执行一些其他的措施,例如提示用户检查输入的文件路径。
Java语言规范将派生于Error类或RuntimeException类的所有异常称为非受检异常,所有其他的异常称为受检异常。受检异常必须要进行处理,而非受检异常既可以处理,也可以不处理。
三.捕获并处理异常
本节将介绍如何使用try、catch和finally块来处理异常。此外,在Java SE 7中,还引入了带资源的try语句,它适用于那些使用可关闭资源的情景。
1.try块
构建一个异常处理器的第一步就是使用try块包围可能出现异常的代码。看下面的例子:
public void createFile() {
File file = new File("D:\bar.txt");
try {
if(file.createNewFile()) {
System.out.println("Create file successfully!");
}
}
catch and finally blocks ...
}
上面的createFile方法的作用是在D盘根目录下创建一个bar.txt文件。在调用File类的createNewFile方法时,可能会出现一个IOException。而这个异常是一个受检异常,必须对它进行处理,因此我们使用一个try块将可能出现异常的语句包围。如果try块中发生异常,则该异常由与其关联的异常处理程序处理。
2.catch块
通过在try块后使用catch块来提供异常处理程序。如果try块中的代码可能出现多种异常,可以使用多个catch块来分别对应不同的异常:
try {
// ...
} catch (ExceptionType name) {
// ...
} catch (ExceptionType name) {
// ...
}
如果在try语句块中的任何代码抛出了一个在catch块中说明的异常类,那么
- 程序将跳过try块的其余代码;
- 程序将执行catch块中的代码;
- 程序继续执行catch块之后的代码。
如果在try块中的代码没有拋出任何异常,那么程序将跳过catch块。
如果try块的代码拋出了一个与任何catch块声明的异常类型都不匹配的异常,那么这个方法就会立刻退出。
下面我们给上面的例子加上catch块:
public void createFile() {
File file = new File("D:\bar.txt");
try {
if(file.createNewFile()) {
System.out.println("Create file successfully!");
}
} catch {
System.out.println("Program threw an exception,please check the path of file.");
}
}
可以在一个try语句块中可以捕获多个异常类型,并对不同类型的异常做出不同的处理。按照下列方式为每个异常类型使用一个单独的catch子句:
在Java SE 7中,同一个catch子句中可以捕获多个异常类型。例如,假设对应缺少文件和未知主机异常的动作是一样的,就可以合并catch子句:
3.finally块
考虑这样一个问题:我们需要与数据库建立连接并执行一些操作,当操作数据库时可能会出现异常,因此我们需要使用try块包围可能出现问题的代码,并在catch块中对这些问题进行处理。无论在操作数据库时是否出现了问题,我们都需要在最后关闭与数据库的连接来释放资源。但关闭数据库连接的操作既不能放在try块中,因为程序可能在此之前就出现异常而进入catch块;也不能放在catch块中,因为程序可能并没有出现异常。此时可以将关闭数据库连接的操作放在finally块中。不管是否有异常被捕获,finally子句中的代码都会被执行。因此,finally块经常被用来关闭资源。在下一小节中,我们将会看到一个更加优雅的关闭资源的方式。
上面的例子可以表示为:
try {
// connect to database
// operate database
} catch {
// handle exception
} finally {
// close connection
}
try语句可以只有finally子句,而没有catch子句。例如:
try {
// connect to database
// operate database
} finally {
// close connection
}
在上面的try-finally结构中,无论在try语句块中是否遇到异常,finally块中的语句都会被执行。如果try块中抛出一个异常,异常会在finally块中的语句执行完之后将异常重新抛出。但是,如果finally块中也出现异常,那么try块中抛出的异常将会被丢弃,程序将会抛出finally块中的异常。
finally块中的return语句
我们知道,finally块中的语句一定会执行,那么是否try块或catch块中的return语句会被finally块中的return语句(如果有的话)覆盖呢?下面来看一个例子:
public static String returnInFinally() {
try {
return "try";
} finally {
return "finally";
}
}
调用这个方法,返回值是"finally"而不是"try"。也就是说,finally块中的return语句会覆盖try或catch块中的return语句。当try块或catch块中的代码执行至return语句时,程序会进入finally块中继续执行,最后执行finally块中的return语句。
finally块中的return语句不仅会覆盖try块和catch块内的返回值,还会丢弃try块或catch块中的异常,就像异常没有发生一样。例如:
public static int returnInFinally() {
try {
int i = 2 / 0;
return i;
} finally {
return 1;
}
}
上面的代码中,2/0将会触发ArithmeticException,但是由于finally块中有return语句,因此这个异常将会被丢弃。
一般来说,应该尽量避免在finally块中使用return语句或抛出异常,除非确实有必要这么做。
4.带资源的try语句
带资源的try语句是指声明了一个或多个资源的try语句。资源是在程序结束时必须关闭的对象。带资源的try语句可以保证在try块结束时关闭资源。任何实现了AutoCloseable接口的对象都可以被看作是资源。
下面的方法从文件中读取第一行,它使用了一个BufferedReader来从文件中读取数据,BufferedReader是程序完成后必须关闭的资源:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在此示例中,try语句中声明的资源是一个BufferedReader对象。声明语句出现在try关键字后面的括号内。BufferedReader类实现了接口AutoCloseable。因为BufferedReader实例是在try语句中声明的,所以无论try语句是正常完成还是出现异常(readLine方法可能会抛出IOException),它都将被关闭。可以这样理解,编译器会自动为我们生成一个finally块并调用资源的close方法来关闭资源。
还可以指定多个资源,例如:
try(Scanner in = new Scanner(new FileInputStream("/usr/share/dict/words"), "UTF-8");
PrintWriter out = new PrintWriter("out.txt")) {
while(in.hasNext()) {
out.println(in.next().toUpperCase());
}
}
当从try块中(无论是否出现异常)退出时,在try语句中所声明的资源的close方法都会被自动调用,并且是按与资源声明相反的顺序来调用的。
不同于try-catch-finally和try-finally,带资源的try语句中,如果try块和关闭资源时同时出现异常,程序将会抛出try块中的异常,而关闭资源时出现的异常将会被抑制,可以通过异常对象的getSuppressed()方法获取被抑制的异常(注意,这里是“抑制”而不是“丢弃”,被丢弃的异常无法通过getSuppressed()获取)。
带资源的try语句也可以有catch块和finally块,不过它们会在资源关闭后才会执行。
5.一个完整的例子
在上面的几小节我们分别学习了try、catch和finally。现在我们来编写一个同时包含这三部分的例子:
public void writeList() {
PrintWriter out = null;
try {
System.out.println("Entering try statement");
out = new PrintWriter(new FileWriter("OutFile.txt");
} catch (IOException e) {
System.err.println("Caught IOException: " + e.getMessage());
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
这个方法的执行顺序无非两种:要么try块中的代码出现异常,程序进入对应的catch块并执行,最后执行finally块中的代码;要么try块中的代码正常结束,然后执行finally块中的代码。
四.声明方法抛出的异常
前面我们讨论了当程序中出现受检异常时应该如何处理。然而,有时候我们不想处理或者需要调用当前方法的方法去处理,此时我们可以不编写异常处理程序,但是需要将可能出现的异常声明在方法名称后面。声明异常的语法如下:
modifiers returnType methodName(parameter list) throws Exception1, Exception2, ...
例如,上面的writeList方法可以改写为:
public void writeList() throw IOException {
PrintWriter out = null;
try {
System.out.println("Entering try statement");
out = new PrintWriter(new FileWriter("OutFile.txt");
} finally {
if (out != null) {
System.out.println("Closing PrintWriter");
out.close();
}
else {
System.out.println("PrintWriter not open");
}
}
}
在上面的程序中,如果try块中的代码抛出异常,由于没有对应的异常处理程序,异常将会继续传递到调用writeList的方法中,在这个方法中也有两种处理方式:要么处理异常,要么继续传递异常。
由于无需为非受检异常编写异常处理程序,因此也就无需将非受检异常声明在方法名称之后。
五.抛出异常
有时候,我们的程序可能本身并没有出现任何异常,但是程序已经进入了错误的逻辑,并且我们需要将这些信息告诉调用当前方法的程序,此时可以手动抛出一个异常。例如,我们编写一个计算平方根的方法,这个方法接受非负整数作为参数。如果其他程序在调用这个方法时传递了一个负数,那么返回任何值都是错误的,此时就需要我们抛出一个异常,告诉该程序当前的错误信息。
使用throw关键字来手动抛出一个异常。如下所示:
public double squareRoot(int x) throws Exception{
if(x < 0) {
throw new Exception("Wrong argument");
}
return Math.sqrt(x);
}
当在其他方法中调用这个方法时,必须处理这个可能出现的异常。
六.异常链
有时我们对异常的操作可能是将其捕获后再抛出新的异常,例如下面的例子:
public class ExceptionChainDemo {
public static void main(String[] args) {
try {
method1();
} catch (Exception e) {
System.out.println("Second stackTrace:");
e.printStackTrace();
}
}
private static void method1() throws Exception {
try {
method2();
} catch (Exception e) {
System.out.println("First stackTrace:");
e.printStackTrace();
throw new Exception("Exception from method1");
}
}
private static void method2() throws Exception {
throw new Exception("Exception from method2");
}
}
该程序的输出如下:
可以看到,method2抛出异常后,在method1中将其捕获并打印调用栈;然后重新抛出另外一个异常,并在main方法中将其捕获并打印调用栈。在第二次打印调用栈时,之前的异常信息已经丢弃,我们只能看到method1中抛出了一个异常。
可以通过链式异常来保存之前的异常信息。也就是说,可以通过之前的异常来构造新的异常。下面是有关的几个方法:
Throwable getCause()
Throwable initCause(Throwable)
Throwable(String, Throwable)
Throwable(Throwable)
getCause方法可以获取被当前异常包装的异常。而initCause方法和另外两个构造方法可以将需要保存的异常包装进当前异常。例如,如果我们要保存method2中抛出的异常,可以像下面这样修改method1:
private static void method1() throws Exception {
try {
method2();
} catch (Exception e) {
System.out.println("First stackTrace:");
e.printStackTrace();
throw new Exception("Exception from method1", e);
}
}
再次运行程序,输入如下:
可以看到,第二次抛出的异常中包含了第一次抛出的异常的信息。
七.自定义异常类
在程序中,可能会遇到任何标准异常类都没有能够充分描述清楚的问题。在这种情况下,我们就可以自己创建一个异常类。
如果需要创建非受检异常,可以创建一个RuntimeException类的子类。当然,实际上很少需要创建非受检异常,更多情况下我们创建的都是受检异常。在创建非受检异常时,需要继承Exception类或其他非受检异常类。
下面自定义了一个受检异常,它继承了IOException:
public class FileFormatException extends IOException {
public FileFormatException() {}
public FileFormatException(String message) {
super(message);
}
}
现在我们就可以在代码中使用自己定义的异常了。