本文尝试以尽可能详细的方式介绍 Java 当中的异常概念和处理机制。本文适合 Java 初学者阅读。
什么是异常
异常是发生在程序运行过程中的,阻断正常流程中的指令执行的事件。
当一个方法在执行当中发生错误时,这个方法就会创建一个特别的对象,将其交付给 Java 运行环境处理。这个对象被称作“异常对象”(或简称异常),它当中包含了异常事件的类型和状态等等信息。 创建这个对象并将其交付给 Java 运行环境的过程,称作“抛出异常”。
异常对象:
所有的异常对象都继承于java.lang.Exception
类。不同类型的异常被定义成它的不同子类。常见的异常类型有java.io.IOException
(读写异常),java.lang.NullPointerException
(空指针异常)等。
当一个方法抛出异常(或“异常对象”)时,运行环境就会尝试寻找某个方法去处理它。所有能够处理该异常的方法,都来自一个叫做“调用堆栈”的方法列表。这个列表的最顶层就是抛出该异常的方法,往下是调用该方法的方法,然后依次类推。
例如a()
方法当中调用了b()
方法,b()
方法当中调用了c()
方法,那么当c()
方法抛出异常时,它就在调用堆栈的最顶层,往下是b()
方法,再往下就是a()
方法。
运行环境会沿着调用堆栈往下寻找,寻找方法当中是否存在能够处理这个异常的代码块。这样的代码块叫做“捕获异常”。运行环境将异常对象交给这个“捕获异常”的代码块处理。如果运行环境在调用堆栈中自始至终未能找到捕获这个异常的代码块,那么整个程序将终止运行。
处理异常的方式:捕获或声明
当你编写一个方法 a(),当中调用了方法 b(),而该方法可能抛出异常,那么你有两种选择:
一、在 a() 方法中用 try-catch 结构捕获 b() 方法抛出的异常,方式如下:
// 示例1
try {
...
b();
...
} catch (Exception e) {
// 处理异常对象
}
二、在 a() 方法上声明为抛出 b() 方法所抛出的异常,像这样:
// 示例2
public void a() throws Exception {
...
b();
...
}
这样的话,调用 a() 方法的那个方法,同样面临两种选择:捕获,或继续抛出。如果你不在这两种处理方式当中选择一种,那么编译器就会判定你的代码存在错误,并拒绝编译。
开始的时候说过,异常事件会阻断指令的执行。也就是说,执行到 b() 方法的时候,如果它抛出了异常,那么它后面的指令都不会执行了。比如在第一种情况下(示例 1)抛出异常,那么从 b();
到 } catch (Exception e) {
之间的所有语句都不会执行;在第二种情况下(示例 2)则是从 b();
到 a() 方法结束之间的所有语句都不会执行。
接下来我们分别介绍这两种处理方式。
捕获异常的方式:try-catch-finally
在 Java 中,捕获异常的方式是编写 try-catch-finally 代码块。下面是一个例子,读取指定文件并将文件内容输出到控制台:
// 示例3
public static void main(String[] args) {
java.io.File file = new File("C:\\1.txt");
java.io.BufferedReader reader = null;
String line;
try {
reader = new BufferedReader(
new java.io.FileReader(file)); // 1
while ((line = reader.readLine()) != null) { // 2
System.out.println(line);
}
} catch (IOException e) { // 3
e.printStackTrace();
} finally { // 4
if (reader != null) {
try {
reader.close(); // 5
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
上面的例子中,1 处的 new FileReader(file)
和 [2] 处的 readLine()
方法都可能抛出 java.io.IOException
,所以我们用 try {...}
将它们包起来,表示这部分代码执行的时候可能抛出异常。
[3] 处为 catch 块,这部分决定了当异常真的发生时,该如何处理。假如 1 处抛出了异常,那么 Java 运行环境将跳过 try 块后面的 while 部分,直接转到 catch 块来处理这个异常。
catch 后面的 (IOException e)
声明了被捕获的异常对象,你可以在 catch 块中使用它。在这里我们简单的调用 e.printStackTrace()
方法,将异常信息输出到控制台。
注意,try 块中抛出的异常类型必须与 catch 块声明所要捕获的异常类型匹配,否则编译器将拒绝编译。关于这一点的详情将在后面说明。
[4] 处为 finally 块,这部分决定了当 try 块和/或 catch 块执行完毕后,还要执行哪些代码,这部分可以看作是“总是会执行的”、“善后工作”。在这里我们尝试关闭 reader 对象,不论是否出现异常。
注意 reader.close();
方法也可能抛出异常,所以 [5] 这里又需要用 try-catch-finally 块包起来。你当然已经注意到,这里没有 finally 部分。其实 try-catch-finally 块有三种写法,分别是:
1. try {...A...} catch(Exception e) {...B...} finally {...C...}
2. try {...A...} catch(Exception e) {...B...}
3. try {...A...} finally {...C...}
写法 1 的意思是:尝试 A;如果出现异常则执行 B;不管是否出现异常,都执行 C。
写法 2 的意思是:尝试 A;如果出现异常则执行 B。
写法 3 的意思是:尝试 A;如果出现异常则不理会抛出去;不管是否出现异常,都执行 C。
在写法 3 中,如果 try 部分抛出了异常,则需要在当前方法中声明。声明抛出异常的方式,将在接下来说明。
声明抛出异常
任何一个方法都可以通过 throws 关键字声明自己可能抛出异常。下面是一个例子:
// 示例4
public void f() throws Exception {
...
}
在这个例子中,方法 f()
声明自己可能会抛出 java.lang.Exception
类型的异常。除了 Exception 外,方法也可以明确的指出自己可能抛出哪种类型的异常,例如:
// 示例5
public void f() throws java.io.IOException {
...
}
如果方法当中的某条语句可能会抛出 A 类型的异常,那么方法就不能声明为抛出 B 类型的异常。下面的做法是错误的:
// 示例6
public void g() throws java.lang.NullPointerException {
f();
}
因为 f()
已经声明为抛出 IOException,而 g()
并没有将该异常捕获,因此它也必须声明为 “throws IOException”。如果编译器发现这种不匹配的情况,就会拒绝编译。
有一种情况例外,就是 B 类型的异常是 A 类型的异常的父类。这意味着 B 类型的异常在业务逻辑上涵盖了 A 类型,所以我们相信凡是能处理 B 的自然也能处理 A。
因此,既然 java.lang.Exception
是所有异常的父类,那么凡是声明抛出该类型异常的方法,自然可以抛出任意类型的异常。也就是说,当一个方法声明为 “throws Exception” 时,它可以抛出任何类型的异常。注意,因为这种声明方式会给方法的调用者带来困扰,所以我们尽量不要这么做。
回顾一下示例 3:
现在回顾一下示例 3 中catch(IOException e) {...}
部分。如果将它改为catch(Exception e) {...}
,就表示捕获所有类型的异常,即它前面的 try 部分不论抛出什么类型的异常,它都可以处理。某些情况下这样做是合理的,因为我们不希望当遇到某个特定种类的异常时,程序终止运行。
接下来请看
Java 异常入门(2/2)