系列中的上一篇
当前教程
捕获和处理异常
系列中的下一篇

系列中的上一篇: 什么是异常?

系列中的下一篇: 抛出异常

捕获和处理异常

 

捕获和处理异常

本节介绍如何使用三个异常处理程序组件(trycatchfinally 块)来编写异常处理程序。然后,解释了在 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 . . .

示例中标记为代码的段包含一个或多个可能抛出异常的合法代码行。(catchfinally 块将在接下来的两个小节中解释。)

要为 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 参数 exfinal,因此您不能在 catch 块中为其分配任何值。

 

Finally 块

finally 块始终在 try 块退出时执行。这确保即使发生意外异常,finally 块也会执行。但 finally 不仅对异常处理有用,它还允许程序员避免清理代码意外地被 returncontinuebreak 绕过。将清理代码放在 finally 块中始终是一个好习惯,即使没有预料到异常。

注意:如果 JVM 在执行 trycatch 代码时退出,则 finally 块可能不会执行。

您一直在使用的 writeList() 方法的 try 块打开了一个 PrintWriter。程序应该在退出 writeList() 方法之前关闭该流。这提出了一个有点复杂的问题,因为 writeList()try 块可以通过三种方式之一退出。

  1. 新的 FileWriter 语句失败并抛出 IOException
  2. list.get(i) 语句失败并抛出 IndexOutOfBoundsException
  3. 一切顺利,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 语句包含两个由分号分隔的声明:ZipFileBufferedWriter。当紧随其后的代码块正常终止或由于异常终止时,close() 方法将按此顺序自动调用 BufferedWriterZipFile 对象。请注意,资源的 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 语句一样具有 catchfinally 块。在 try-with-resources 语句中,任何 catchfinally 块都在声明的资源关闭后运行。

 

被抑制的异常

可以从与 try-with-resources 语句关联的代码块中抛出异常。在示例 writeToFileZipFileContents() 中,可以从 try 块中抛出异常,并且当 try-with-resources 语句尝试关闭 ZipFileBufferedWriter 对象时,最多可以从 try-with-resources 语句中抛出两个异常。如果从 try 块中抛出异常,并且从 try-with-resources 语句中抛出一个或多个异常,那么从 try-with-resources 语句中抛出的这些异常将被抑制,并且由 writeToFileZipFileContents() 方法抛出的异常是块抛出的异常。您可以通过调用从 try 块抛出的异常的 Throwable.getSuppressed() 方法来检索这些被抑制的异常。

 

实现 AutoCloseable 或 Closeable 接口的类

有关实现这两个接口之一的类的列表,请参见 AutoCloseableCloseable 接口的 Javadoc。 Closeable 接口扩展了 AutoCloseable 接口。 close() 方法的 Closeable 接口抛出类型为 IOException 的异常,而 close() 方法的 AutoCloseable 接口抛出类型为 Exception 的异常。因此, AutoCloseable 接口的子类可以覆盖 close() 方法的这种行为,以抛出专门的异常(例如 IOException)或根本不抛出异常。

 

综合起来

前面的部分描述了如何为 ListOfNumbers 类中的 writeList() 方法构造 trycatchfinally 代码块。现在,让我们逐步浏览代码并研究可能发生的情况。

当所有组件组合在一起时,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 块有三种不同的退出可能性;以下是其中两种。

  1. try 语句中的代码失败并抛出异常。这可能是由新的 FileWriter 语句引起的 IOException,也可能是由 for 循环中错误的索引值引起的 IndexOutOfBoundsException
  2. 一切成功,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 日


系列中的上一篇
当前教程
捕获和处理异常
系列中的下一篇

系列中的上一篇: 什么是异常?

系列中的下一篇: 抛出异常