乐趣区

Java™ 教程(捕获和处理异常)

捕获和处理异常
本节描述如何使用三个异常处理程序组件 — try、catch 和 finally 块 — 来编写异常处理程序,然后,解释了 Java SE 7 中引入的 try-with-resources 语句,try-with-resources 语句特别适用于使用 Closeable 资源的情况,例如流。
本节的最后一部分将介绍一个示例,并分析各种场景中发生的情况。
以下示例定义并实现名为 ListOfNumbers 的类,构造时,ListOfNumbers 创建一个 ArrayList,其中包含 10 个具有顺序值 0 到 9 的整数元素,ListOfNumbers 类还定义了一个名为 writeList 的方法,该方法将数字列表写入名为 OutFile.txt 的文本文件中,此示例使用 java.io 中定义的输出类,这些类在基础 I / O 中介绍。
// Note: This class will not compile yet.
import java.io.*;
import java.util.List;
import java.util.ArrayList;

public class ListOfNumbers {

private List<Integer> list;
private static final int SIZE = 10;

public ListOfNumbers () {
list = new ArrayList<Integer>(SIZE);
for (int i = 0; i < SIZE; i++) {
list.add(new Integer(i));
}
}

public void writeList() {
// The FileWriter constructor throws IOException, which must be caught.
PrintWriter out = new PrintWriter(new FileWriter(“OutFile.txt”));

for (int i = 0; i < SIZE; i++) {
// The get(int) method throws IndexOutOfBoundsException, which must be caught.
out.println(“Value at: ” + i + ” = ” + list.get(i));
}
out.close();
}
}
FileWriter 构造函数初始化文件上的输出流,如果无法打开该文件,则构造函数将抛出 IOException。对 ArrayList 类的 get 方法的调用,如果其参数的值太小(小于 0)或太大(大于 ArrayList 当前包含的元素数),则抛出 IndexOutOfBoundsException。
如果你尝试编译 ListOfNumbers 类,编译器将输出有关 FileWriter 构造函数抛出的异常的错误消息,但是,它不会显示有关 get 抛出的异常的错误消息,原因是构造函数抛出的异常 IOException 是一个经过检查的异常,而 get 方法抛出的异常(IndexOutOfBoundsException)是一个未经检查的异常。
现在你已熟悉 ListOfNumbers 类以及可以在其中抛出异常的位置,你已准备好编写异常处理程序来捕获和处理这些异常。
try 块
构造异常处理程序的第一步是使用 try 块将可能引发异常的代码括起来,通常,try 块如下所示:
try {
code
}
catch and finally blocks . . .
标记为 code 的示例段中包含一个或多个可能引发异常的合法代码行(catch 和 finally 块将在接下来的两个小节中解释)。
要从 ListOfNumbers 类构造 writeList 方法的异常处理程序,请将 writeList 方法的异常抛出语句包含在 try 块中,有不止一种方法可以做到这一点,你可以将可能引发异常的每行代码放在其自己的 try 块中,并为每个代码提供单独的异常处理程序。或者,你可以将所有 writeList 代码放在一个 try 块中,并将多个处理程序与它相关联,以下列表对整个方法使用一个 try 块,因为所讨论的代码非常短。
private List<Integer> list;
private static final int SIZE = 10;

public void writeList() {
PrintWriter out = null;
try {
System.out.println(“Entered try statement”);
out = new PrintWriter(new FileWriter(“OutFile.txt”));
for (int i = 0; i < SIZE; i++) {
out.println(“Value at: ” + i + ” = ” + list.get(i));
}
}
catch and finally blocks . . .
}
如果 try 块中发生异常,则该异常由与之关联的异常处理程序处理,要将异常处理程序与 try 块关联,必须在其后面放置一个 catch 块。
catch 块
通过在 try 块之后直接提供一个或多个 catch 块,可以将异常处理程序与 try 块关联,try 块的末尾和第一个 catch 块的开头之间不能有代码。
try {

} catch (ExceptionType name) {

} catch (ExceptionType name) {

}
每个 catch 块都是一个异常处理程序,它处理由其参数表示的异常类型,参数类型 ExceptionType 声明了处理程序可以处理的异常类型,并且必须是从 Throwable 类继承的类的名称,处理程序可以使用 name 引用异常。
catch 块包含在调用异常处理程序时执行的代码,当处理程序是调用堆栈中的第一个 ExceptionType 与抛出的异常类型匹配时,运行时系统调用此异常处理程序,如果抛出的对象可以合法地分配给异常处理程序的参数,则系统认为它是匹配的。
以下是 writeList 方法的两个异常处理程序:
try {

} catch (IndexOutOfBoundsException e) {
System.err.println(“IndexOutOfBoundsException: ” + e.getMessage());
} catch (IOException e) {
System.err.println(“Caught IOException: ” + e.getMessage());
}
异常处理程序不仅可以打印错误消息或停止程序,它们可以执行错误恢复、提示用户做出决定,或使用链式异常将错误传播到更高级别的处理程序,如“链式异常”部分所述。
使用一个异常处理程序捕获多种类型的异常
在 Java SE 7 及更高版本中,单个 catch 块可以处理多种类型的异常,此功能可以减少代码重复并减少捕获过于宽泛的异常的诱惑。
在 catch 子句中,指定块可以处理的异常类型,并使用竖线(|)分隔每个异常类型:
catch (IOException|SQLException ex) {
logger.log(ex);
throw ex;
}
注意:如果 catch 块处理多个异常类型,则 catch 参数隐式为 final,在此示例中,catch 参数 ex 是 final,因此你无法在 catch 块中为其分配任何值。
finally 块
当 try 块退出时,finally 块总是执行,这确保即使发生意外异常也会执行 finally 块,但 finally 不仅仅是异常处理有用 — 它允许程序员避免因 return、continue 或 break 而意外绕过清理代码,将清理代码放在 finally 块中始终是一种很好的做法,即使没有预期的异常情况也是如此。
注意:如果在执行 try 或 catch 代码时 JVM 退出,则 finally 块可能无法执行,同样,如果执行 try 或 catch 代码的线程被中断或终止,则即使应用程序作为一个整体继续,finally 块也可能无法执行。
你在此处使用的 writeList 方法的 try 块打开了 PrintWriter,程序应该在退出 writeList 方法之前关闭该流,这带来了一个有点复杂的问题,因为 writeList 的 try 块可以以三种方式之一退出。

new FileWriter 语句失败并抛出 IOException。

list.get(i) 语句失败并抛出 IndexOutOfBoundsException。
一切都成功,try 块正常退出。

无论 try 块中发生了什么,运行时系统总是执行 finally 块中的语句,所以这是进行清理的最佳地点。
下面的 writeList 方法的 finally 块清理然后关闭 PrintWriter。
finally {
if (out != null) {
System.out.println(“Closing PrintWriter”);
out.close();
} else {
System.out.println(“PrintWriter not open”);
}
}
重要提示:finally 块是防止资源泄漏的关键工具,关闭文件或以其他方式恢复资源时,将代码放在 finally 块中以确保始终恢复资源。请考虑在这些情况下使用 try-with-resources 语句,这会在不再需要时自动释放系统资源,try-with-resources 语句部分提供了更多信息。

try-with-resources 语句
try-with-resources 语句是一个声明一个或多个资源的 try 语句,资源是在程序完成后必须关闭的对象,try-with-resources 语句确保在语句结束时关闭每个资源,实现 java.lang.AutoCloseable 的任何对象(包括实现 java.io.Closeable 的所有对象)都可以用作资源。
以下示例从文件中读取第一行,它使用 BufferedReader 实例从文件中读取数据,BufferedReader 是一个在程序完成后必须关闭的资源:
static String readFirstLineFromFile(String path) throws IOException {
try (BufferedReader br =
new BufferedReader(new FileReader(path))) {
return br.readLine();
}
}
在此示例中,try-with-resources 语句中声明的资源是 BufferedReader,声明语句出现在 try 关键字后面的括号内,Java SE 7 及更高版本中的 BufferedReader 类实现了 java.lang.AutoCloseable 接口,因为 BufferedReader 实例是在 try-with-resource 语句中声明的,所以无论 try 语句是正常完成还是突然完成(由于 BufferedReader.readLine 方法抛出 IOException),它都将被关闭。
在 Java SE 7 之前,你可以使用 finally 块来确保关闭资源,无论 try 语句是正常还是突然完成,以下示例使用 finally 块而不是 try-with-resources 语句:
static String readFirstLineFromFileWithFinallyBlock(String path)
throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
try {
return br.readLine();
} finally {
if (br != null) br.close();
}
}
但是,在此示例中,如果方法 readLine 和 close 都抛出异常,则方法 readFirstLineFromFileWithFinallyBlock 抛出 finally 块抛出的异常,从 try 块抛出的异常被抑制。相反,在示例 readFirstLineFromFile 中,如果从 try 块和 try-with-resources 语句抛出异常,则 readFirstLineFromFile 方法抛出 try 块抛出的异常,从 try-with-resources 块抛出的异常被抑制,在 Java SE 7 及更高版本中,你可以检索已抑制的异常,有关详细信息,请参阅“抑制的异常”部分。
你可以在 try-with-resources 语句中声明一个或多个资源,以下示例检索 zip 文件 zipFileName 中打包的文件的名称,并创建包含这些文件名称的文本文件:
public static void writeToFileZipFileContents(String zipFileName,
String outputFileName)
throws java.io.IOException {

java.nio.charset.Charset charset =
java.nio.charset.StandardCharsets.US_ASCII;
java.nio.file.Path outputFilePath =
java.nio.file.Paths.get(outputFileName);

// Open zip file and create output file with
// try-with-resources statement

try (
java.util.zip.ZipFile zf =
new java.util.zip.ZipFile(zipFileName);
java.io.BufferedWriter writer =
java.nio.file.Files.newBufferedWriter(outputFilePath, charset)
) {
// Enumerate each entry
for (java.util.Enumeration entries =
zf.entries(); entries.hasMoreElements();) {
// Get the entry name and write it to the output file
String newLine = System.getProperty(“line.separator”);
String zipEntryName =
((java.util.zip.ZipEntry)entries.nextElement()).getName() +
newLine;
writer.write(zipEntryName, 0, zipEntryName.length());
}
}
}
在此示例中,try-with-resources 语句包含两个以分号分隔的声明:ZipFile 和 BufferedWriter,当直接跟随它的代码块正常或由于异常而终止时,将按此顺序自动调用 BufferedWriter 和 ZipFile 对象的 close 方法,请注意,资源的 close 方法按其创建的相反顺序调用。
以下示例使用 try-with-resources 语句自动关闭 java.sql.Statement 对象:
public static void viewTable(Connection con) throws SQLException {

String query = “select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES”;

try (Statement stmt = con.createStatement()) {
ResultSet rs = stmt.executeQuery(query);

while (rs.next()) {
String coffeeName = rs.getString(“COF_NAME”);
int supplierID = rs.getInt(“SUP_ID”);
float price = rs.getFloat(“PRICE”);
int sales = rs.getInt(“SALES”);
int total = rs.getInt(“TOTAL”);

System.out.println(coffeeName + “, ” + supplierID + “, ” +
price + “, ” + sales + “, ” + total);
}
} catch (SQLException e) {
JDBCTutorialUtilities.printSQLException(e);
}
}
此示例中使用的资源 java.sql.Statement 是 JDBC 4.1 及更高版本 API 的一部分。
注意:try-with-resources 语句可以像普通的 try 语句一样有 catch 和 finally 块,在 try-with-resources 语句中,在声明的资源关闭后运行任何 catch 或 finally 块。
抑制异常
可以从与 try-with-resources 语句关联的代码块中抛出异常,在示例 writeToFileZipFileContents 中,可以从 try 块抛出异常,当 try-with-resources 语句尝试关闭 ZipFile 和 BufferedWriter 对象时,最多可以抛出两个异常。如果从 try 块抛出异常,并且从 try-with-resources 语句中抛出一个或多个异常,则会抑制从 try-with-resources 语句抛出的那些异常,并且块抛出的异常是 writeToFileZipFileContents 方法抛出的异常,你可以通过从 try 块抛出的异常中调用 Throwable.getSuppressed 方法来检索这些抑制的异常。
实现 AutoCloseable 或 Closeable 接口的类
请参阅 AutoCloseable 和 Closeable 接口的 Javadoc,以获取实现这些接口之一的类列表,Closeable 接口扩展了 AutoCloseable 接口。Closeable 接口的 close 方法抛出 IOException 类型的异常,而 AutoCloseable 接口的 close 方法抛出异常类型 Exception,因此,AutoCloseable 接口的子类可以覆盖 close 方法的这种行为,以抛出专门的异常,例如 IOException,或者根本没有异常。
把它们放在一起
前面的部分描述了如何为 ListOfNumbers 类中的 writeList 方法构造 try、catch 和 finally 代码块,现在,让我们来看看代码并调查会发生什么。
将所有组件放在一起时,writeList 方法如下所示。
public void writeList() {
PrintWriter out = null;

try {
System.out.println(“Entering” + ” try statement”);

out = new PrintWriter(new FileWriter(“OutFile.txt”));
for (int i = 0; i < SIZE; i++) {
out.println(“Value at: ” + i + ” = ” + list.get(i));
}
} catch (IndexOutOfBoundsException e) {
System.err.println(“Caught IndexOutOfBoundsException: ”
+ e.getMessage());

} 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 块有三种不同的退出可能性,这是其中两个。

try 语句中的代码失败并引发异常,这可能是由 new FileWriter 语句引起的 IOException 或由 for 循环中的错误索引值引起的 IndexOutOfBoundsException。
一切都成功,try 语句正常退出。

场景 1:发生异常
创建 FileWriter 的语句可能由于多种原因而失败,例如,如果程序无法创建或写入指示的文件,则 FileWriter 的构造函数将抛出 IOException。
当 FileWriter 抛出 IOException 时,运行时系统立即停止执行 try 块,正在执行的方法调用未完成,然后,运行时系统开始在方法调用堆栈的顶部搜索适当的异常处理程序。在此示例中,发生 IOException 时,FileWriter 构造函数位于调用堆栈的顶部,但是,FileWriter 构造函数没有适当的异常处理程序,因此运行时系统在方法调用堆栈中检查下一个方法 — writeList 方法,writeList 方法有两个异常处理程序:一个用于 IOException,另一个用于 IndexOutOfBoundsException。
运行时系统按照它们在 try 语句之后出现的顺序检查 writeList 的处理程序,第一个异常处理程序的参数是 IndexOutOfBoundsException,这与抛出的异常类型不匹配,因此运行时系统会检查下一个异常处理程序 — IOException,这与抛出的异常类型相匹配,因此运行时系统结束搜索适当的异常处理程序,既然运行时已找到适当的处理程序,那么执行该 catch 块中的代码。
异常处理程序执行后,运行时系统将控制权传递给 finally 块,无论上面捕获的异常如何,finally 块中的代码都会执行,在这种情况下,FileWriter 从未打开过,不需要关闭,在 finally 块完成执行后,程序继续执行 finally 块之后的第一个语句。
这是抛出 IOException 时出现的 ListOfNumbers 程序的完整输出。
Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open
以下清单中的后加 * 号的代码显示了在此方案中执行的语句:
public void writeList() {
PrintWriter out = null; //******

try {
System.out.println(“Entering try statement”); //*******
out = new PrintWriter(new FileWriter(“OutFile.txt”)); //******
for (int i = 0; i < SIZE; i++)
out.println(“Value at: ” + i + ” = ” + list.get(i));

} catch (IndexOutOfBoundsException e) {
System.err.println(“Caught IndexOutOfBoundsException: ”
+ e.getMessage());

} 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”); /******
}
}
}
场景 2:try 块正常退出
在这种情况下,try 块范围内的所有语句都成功执行,并且不会抛出异常,执行在 try 块的末尾结束,运行时系统将控制传递给 finally 块,因为一切都成功了,所以当控制到达 finally 块时,PrintWriter 会打开,这会关闭 PrintWriter,同样,在 finally 块完成执行之后,程序继续执行 finally 块之后的第一个语句。
当没有抛出异常时,这是 ListOfNumbers 程序的输出。
Entering try statement
Closing PrintWriter
以下示例中的加 * 号的代码显示了在此方案中执行的语句。
public void writeList() {
PrintWriter out = null; //*****
try {
System.out.println(“Entering try statement”); //******
out = new PrintWriter(new FileWriter(“OutFile.txt”)); //******
for (int i = 0; i < SIZE; i++) //******
out.println(“Value at: ” + i + ” = ” + list.get(i)); //******

} catch (IndexOutOfBoundsException e) {
System.err.println(“Caught IndexOutOfBoundsException: ”
+ e.getMessage());

} 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”);
}
}
}

上一篇:捕获或指定要求

退出移动版