捕获和处理异常
捕获和处理异常
本节介绍如何使用三个异常处理程序组件(try
、catch
和 finally
块)来编写异常处理程序。然后,解释了在 Java SE 7 中引入的 try-with-resources 语句。try-with-resources 语句特别适用于使用 Closeable
资源(如流)的情况。
本节的最后一部分将逐步介绍一个示例,并分析在各种情况下发生的事件。
以下示例定义并实现了一个名为 ListOfNumbers
的类。在构造时,ListOfNumbers
创建一个 ArrayList
,其中包含 10 个 Integer
元素,其顺序值为 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<>(SIZE);
for (int i = 0; i < SIZE; i++) {
list.add(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();
}
}
第一行粗体字是调用构造函数。构造函数在文件上初始化输出流。如果无法打开文件,构造函数将抛出 IOException
。第二行粗体字是调用 ArrayList
类的 get 方法,如果其参数的值太小(小于 0)或太大(大于 ArrayList
当前包含的元素数量),该方法将抛出 IndexOutOfBoundsException
。
如果尝试编译 ListOfNumbers
类,编译器将打印有关 FileWriter
构造函数抛出的异常的错误消息。但是,它不会显示有关 get()
抛出的异常的错误消息。原因是构造函数抛出的异常 IOException
是一个已检查异常,而 get()
方法抛出的异常 IndexOutOfBoundsException
是一个未检查异常。
现在您已经熟悉了 ListOfNumbers
类以及其中可能抛出异常的位置,您就可以编写异常处理程序来捕获和处理这些异常。
Try 块
构造异常处理程序的第一步是将可能抛出异常的代码包含在 try
块中。一般来说,try
块如下所示
try {
code
}
catch and finally blocks . . .
示例中标记为代码的段包含一个或多个可能抛出异常的合法代码行。(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 块”将向您展示如何操作。
Catch 块
您可以通过在 try
块之后直接提供一个或多个 catch 块来将异常处理程序与 try
块相关联。try
块的末尾和第一个 catch
块的开头之间不能有代码。
try {
} catch (ExceptionType name) {
} catch (ExceptionType name) {
}
每个 catch
块都是一个异常处理程序,它处理其参数指示的异常类型。参数类型 ExceptionType
声明处理程序可以处理的异常类型,并且必须是继承自 Throwable
类的类的名称。处理程序可以使用名称引用异常。
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 块
finally
块始终在 try
块退出时执行。这确保即使发生意外异常,finally
块也会执行。但 finally
不仅对异常处理有用,它还允许程序员避免清理代码意外地被 return
、continue
或 break
绕过。将清理代码放在 finally
块中始终是一个好习惯,即使没有预料到异常。
注意:如果 JVM 在执行
try
或catch
代码时退出,则finally
块可能不会执行。
您一直在使用的 writeList()
方法的 try
块打开了一个 PrintWriter
。程序应该在退出 writeList()
方法之前关闭该流。这提出了一个有点复杂的问题,因为 writeList()
的 try
块可以通过三种方式之一退出。
- 新的
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 {
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
。当紧随其后的代码块正常终止或由于异常终止时,close()
方法将按此顺序自动调用 BufferedWriter
和 ZipFile
对象。请注意,资源的 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-with-resources 语句中抛出两个异常。如果从 try
块中抛出异常,并且从 try-with-resources 语句中抛出一个或多个异常,那么从 try-with-resources 语句中抛出的这些异常将被抑制,并且由 writeToFileZipFileContents()
方法抛出的异常是块抛出的异常。您可以通过调用从 try
块抛出的异常的 Throwable.getSuppressed()
方法来检索这些被抑制的异常。
实现 AutoCloseable 或 Closeable 接口的类
有关实现这两个接口之一的类的列表,请参见 AutoCloseable
和 Closeable
接口的 Javadoc。 Closeable
接口扩展了 AutoCloseable
接口。 close()
方法的 Closeable
接口抛出类型为 IOException
的异常,而 close()
方法的 AutoCloseable
接口抛出类型为 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
语句中的代码失败并抛出异常。这可能是由新的FileWriter
语句引起的IOException
,也可能是由for
循环中错误的索引值引起的IndexOutOfBoundsException
。- 一切成功,
try
语句正常退出。
让我们看看在这两种退出可能性期间,writeList()
方法中发生了什么。
场景 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
场景 2:try 块正常退出
在这种情况下,try
块范围内的所有语句都成功执行,没有抛出异常。执行从 try
块的末尾退出,运行时系统将控制权传递给 finally
块。由于一切顺利,PrintWriter
在控制权到达 finally
块时处于打开状态,该块会关闭 PrintWriter
。同样,在 finally
块执行完毕后,程序将继续执行 finally
块后的第一个语句。
以下是 ListOfNumbers
程序在没有抛出异常时的输出。
Entering try statement
Closing PrintWriter
最后更新: 2021 年 9 月 14 日