try语句(try statement)主要用于执行一些可能触发异常(Exception)或者错误(Error)的代码。当使用try...catch语句时,能帮助开发人员捕捉发生的异常或者错误。当使用try-with-resource语句时,能帮助开发人员更好的保护所使用的资源。所以,本章除了介绍try语句的用法以外,还会讲解Java编译器是如何处理try语句的。
try语句有两种形式。第一种形式是try...catch...finally。其中,try包含所需执行的代码块;catch子句是可选的,用于检测与匹配执行过程中发生的异常;finally部分也是可选的,无论在执行过程中是否发生异常,如果发生了异常,无论catch部分是否成功捕获异常,finally部分的代码都被会执行。这是由Java编译器保证的。虽然catch子句与finally子句都是可选的,但是这种形式的try语句必须至少包含一个catch子句或者finally子句。catch子句和finally子句不能同时省略。
如下面的例子所示,Java虚拟机会先执行try子句的代码块,打开/tmp/example.txt文件,并获取当前有多少个字符已被加载至缓冲区中。如果在此过程中,程序发生IOException异常,则会被catch子句捕抓,并执行catch子句的代码,答应错误信息。最后,Java虚拟机执行finally子句,关闭已打开的文件流。
import java.io.FileInputStream;
import java.io.IOException;
public class TryExample {
public static void main(String[] args) {
FileInputStream in = null;
try {
in = new FileInputStream("/tmp/example.txt");
int available = in.available(); // 获取多少字符已被加载至缓冲区中
} catch (IOException ex) {
// 如果抓捕到了IOException异常
System.out.println(ex.getMessage());
} finally {
try {
if (in != null)
in.close();
} catch (IOException ex) {
System.out.println("Can't close the file stream.");
}
}
}
}
Java虚拟机在依次执行try部分的代码时,如果发生异常,Java虚拟机会忽略try部分的后续代码,直接跳到catch部分,检查发生的异常是否与catch子句中捕抓的异常匹配。在try语句中,开发人员可以使用多个catch子句(Catch Clause)捕抓多个不同的异常。或者,开发人员也可以使用一个catch子句,捕抓多个不同的异常。异常匹配是指发生的异常类型与捕抓异常的类型相同,或者是其子类。Java虚拟机会依次检查catch子句中给出的异常类型,直到发现第一个匹配的异常为止,并进入相对应的异常处理代码块。如果在catch子句中未发现匹配的异常,则可认为该try语句抛出了这个未捕获的异常,从而跳到上一级try语句的catch子句处继续查找匹配的异常,直至找到为止,或者程序异常退出。
try语句的执行结果可按照以下几类场景确定,我们用表来描述这些场景。在表中,除了场景序号以外,其他四栏分别表示try子句的执行结果,catch子句的执行结果,finally子句的执行结果,和整个try语句的执行结果。例如,第一个场景可以理解为:当try子句执行成功时,如果catch子句被省略,并且finally子句也执行成功,那么,整个try语句可被认为执行成功。以此类推其他场景。
场景 | try子句执行结果 | catch子句执行结果 | finally子句执行结果 | try语句执行结果 |
---|---|---|---|---|
1 | 执行成功 | 省略 | 执行成功 | 执行成功 |
2 | 执行成功 | 省略 | 执行失败,抛出异常,但未能成功捕获异常 | 执行失败,抛出未捕获的异常 |
3 | 执行成功 | 省略 | 执行失败,抛出异常,但成功捕获该异常,则可认为finally子句执行成功 | 执行成功 |
4 | 执行失败,抛出了异常 | 未能捕获该异常 | 省略 | 执行失败,抛出未捕获的异常 |
5 | 执行失败,抛出了异常 | 未能捕获该异常 | 执行成功 | 执行失败,抛出未捕获的异常 |
6 | 执行失败,抛出了异常 | 成功捕获异常,并执行成功 | 省略 | 执行成功 |
7 | 执行失败,抛出了异常 | 成功捕获异常,但在执行catch子句的过程中抛出了另一个异常 | 省略 | 执行失败,抛出未捕获的异常 |
8 | 执行失败,抛出了异常 | 成功捕获异常。在执行catch子句的过程又抛出了另一个异常,但成功捕获的这个异常 | 省略 | 执行成功 |
9 | 执行失败,抛出了异常 | 省略 | 执行成功 | 执行失败,抛出未捕获的异常 |
10 | 执行失败,抛出了异常 | 省略 | 执行失败,抛出了另一个异常 | 执行失败,抛出finally子句中的异常。try子句中的异常会被丢弃。 |
11 | 执行失败,抛出了异常 | 成功捕获异常。但在执行过程中,又抛出了另一个异常 | 省略或者执行成功 | 执行失败,抛出catch子句中的未捕获的异常 |
12 | 执行失败,抛出了异常 | 成功捕获异常。但在执行过程中,又抛出了另一个异常 | 执行失败,抛出了第三个异常 | 执行失败,抛出finally子句中的异常。catch子句中的异常会被丢弃。 |
下面是两个try...catch...finally语句的例子。第一个例子在一个catch子句中尝试捕抓IndexOutOfBoundsException和NumberFormatException异常。第二个例子在两个catch子句中分别捕抓这两个异常。在第二个例子中,异常对象可以都命名为ex,它们并不会相互冲突,因为它们不在同一个作用域内。
public class TryExample {
public static void main(String[] args) {
// Example 1
try{
String str = "100";
String strOfLastDigit = str.substring(2);
Integer i = Integer.valueOf(strOfLastDigit);
System.out.println("Last digit is " + i);
} catch (IndexOutOfBoundsException | NumberFormatException ex) {
// Exception Handler
System.out.println(ex.getMessage());
}
// Example 2
try{
String str = "100";
String strOfLastDigit = str.substring(2);
Integer i = Integer.valueOf(strOfLastDigit);
System.out.println("Last digit is " + i);
} catch (IndexOutOfBoundsException ex) {
// ClassNotFoundException Handler
System.out.println(ex.getMessage());
} catch (NumberFormatException ex) {
// ClassNotFoundException Handler
System.out.println(ex.getMessage());
}
}
}
try-with-resource语句有两种形式。第一种形式的try-with-resource语句可以在括号中初始化一个资源变量。变量初始化的过程会在try代码块运行前执行。在try代码块执行完毕后,资源变量所表示的资源会自动释放。资源释放是通过java.lang.AutoCloseable接口实现的。资源变量的类型需要实现AutoCloseable接口,Java虚拟机会自动调用这个接口释放资源。第二种形式的try-with-resource语句可直接使用一个已初始化的资源变量。在try代码块运行完毕后,Java虚拟机会自动释放资源。第二种形式的try-with-resource语句在Java 9中引入。
值得注意的是,这里的资源变量必须是final变量或者可被认为是final的变量(effectively final,这种变量是可以被声明为final,但是开发人员没有将其声明为final的变量)。如果开发人员使用了第二种形式的try-with-resource语句,Java编译器会定义一个临时变量,并将其转换为第一种形式的try-with-resource语句。
当资源变量初始化时抛出了异常,则try语句的代码块不会被执行。整个try语句会被认为执行失败,抛出这个异常。try语句无需释放资源。当资源变量初始化成功,但是在执行try代码块的时候失败,抛出异常,则停止执行剩余的try代码块中的代码,资源被自动释放,整个try语句抛出这个异常。
如下面的代码所示,readFile2成员方法使用的是第二种形式的try-with-resource语句。实际上,它会被Java编译器转化为readFile3成员方法中所示的代码。
import java.io.IOException;
import java.io.FileInputStream;
public class TryExample {
public void readFile1(String filename) throws IOException {
try (FileInputStream input = new FileInputStream(filename)) {
int avail = input.available();
}
// input is closed.
}
public void readFile2(String filename) throws IOException {
FileInputStream input = new FileInputStream(filename);
try (input) {
int avail = input.available();
}
// input is closed.
}
public void readFile3(String filename) throws IOException {
FileInputStream input = new FileInputStream(filename);
try (FileInputStream tempInput = input) {
int avail = input.available();
}
// input/tempInput is closed.
}
}
更复杂的情况是,try-with-resource语句可接受多个资源变量。在这种情况下,Java编译器会按照顺序将这个包含多个资源的try-with-resource语句变成多个包含单个资源的try-with-resource语句,并将每个语句依次嵌入在另一个语句里。如下面的例子所示,Java编译器会将readFile1函数内的try-with-resource语句转化为readFile2函数中的语句。因此,try语句的嵌套层次也确定了多个资源初始化的顺序和释放的顺序。
import java.io.IOException;
import java.io.FileInputStream;
public class TryExample {
public void readFile1(String filename1, String filename2) throws IOException {
try (FileInputStream input1 = new FileInputStream(filename1);
FileInputStream input2 = new FileInputStream(filename2)
) {
int aval1 = input1.available();
int aval2 = input2.available();
}
// input1 and input2 are closed.
}
public void readFile2(String filename1, String filename2) throws IOException {
try (FileInputStream input1 = new FileInputStream(filename1)) {
try (FileInputStream input2 = new FileInputStream(filename2)) {
int aval1 = input1.available();
int aval2 = input2.available();
}
// input 2 is closed
}
// input1 is closed.
}
}
try-with-resource语句也可使用catch子句和finally子句,其使用方法和try...catch...finally语句类似。当遇到try-with-resource语句的扩展形式时,Java编译器会使用try-with-resource的基本形式(见本章3.1)将资源保护起来,然后将其放置在try...catch...finally语句中。如下面的例子所示,Java编译器会将readFile1成员方法中的语句转换成readFile2成员方法中的语句。
import java.io.IOException;
import java.io.FileInputStream;
public class TryExample {
public void readFile1(String filename) throws IOException {
try (FileInputStream input = new FileInputStream(filename)) {
int avail = input.available();
} catch (IOException ex) {
System.out.println("An IOException occurs.");
} finally {
System.out.println("finally clause is invoked.");
}
}
public void readFile2(String filename) throws IOException {
try {
try (FileInputStream input = new FileInputStream(filename)) {
int avail = input.available();
}
} catch (IOException ex) {
System.out.println("An IOException occurs.");
} finally {
System.out.println("finally clause is invoked.");
}
}
}
在这种情况下的异常处理与try...catch...finally语句的处理类似。如果在内层的try-with-resource语句执行失败,则可认为是外层try代码块执行失败,Java编译器会在catch子句中寻找匹配的异常处理代码块,并在最后执行finally子句。
唯一的一个例外是Suppressed Exception List。当执行try-with-resource语句时,如果资源变量初始化发生异常Ex1或者try代码块发生异常Ex1,而且在释放资源时也发生了异常Ex2,此时,try-with-resource语句会被认为执行失败,抛出异常Ex1。但是,Ex2不会被丢弃。Ex2会被当做Suppressed Exception加入到Ex1的Suppressed Exception List中。当在处理Ex1异常时,Ex2异常对象可通过Ex1.getSuppressed()获取。类似的,当释放多个资源时,如果发生了两个以上的异常,则后发生的异常会被加入到第一个异常的Suppressed Exception List中。因此,try-with-resource语句会被认为执行失败,抛出第一个发生的异常。
本章介绍了try语句的两种形式,以及几种基本的变化和用法。从Java编译器和虚拟机的角度看,这两种try语句的形式是可以相互转换的,其对于异常的处理过程也类似。try-with-resource语句为开发人员提供了语法上的便利,但是,在开发过程中,开发人员需要注意如何处理资源释放时可能产生的异常。
一般来说,这种情况可采用两种策略。第一种策略思想是,如果释放资源时发生异常,说明该资源已处于错误状态。在这种情况下,程序可以忽略该错误(捕抓该错误,但是并不处理该错误)。
另一种策略思想是,既然资源释放时产生错误,程序应该对其进行额外的保护,要么确保以后不会再使用该资源,要么将其暂时保留,等待其恢复后,再释放。这两种策略无优劣之分,可按照应用场景选取。
注册用户登陆后可留言